回顾
上次我们讲到了策略模式,我们略微一回顾: 策略模式是指将项目中经常变化的部分,抽取定义为算法族接口,这样它们互不影响并可以相互替换,独立于使用算法的客户。 冰冻三尺,非一日之寒 仅仅知道OO当中的继承、抽象、多态这些概念,并不会让你立马成为一个好的设计者。要构造具有健壮、可扩展、弹性好的系统绝不是一日之功。事实证明只有不断艰苦实践才有可能。 我们接下来继续学习观察者模式。
观察者(Observer)模式
观察者模式是一种对象行为模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会接到通知,并更新自己。 我发现有些人急性子,可能是只想看下概念,所以我这次直接先把比较严格的定义放在前面。
它适合的场景
当一个对象改变时需要通知其它对象,而且不知道具体有多少对象。
优点:
1.将对象解耦,把观察者与被观察对象完全隔离。 2.可以快速接入多个观察者,而不影响其它观察者。
缺点
1.多个观察者会增加类的数量,造成维护上难度增加。 2.Java中消息通知一般是顺序执行的,其中一个观察者阻塞,会影响整体的效率。 3.另外还要特别注意,在被观察者之间循环的问题,要避免这一点。
气象监测应用
我们来看一个气象检测应用,此系统中的三个部分是:
1.气象站(获取实际气象数据的物理装置)。
2.WeatherData对象(追踪来自气象站的数据,并更新到布告板)。
3.三个布告板(显示目前天气状况给用户看的),未来可能会增加数量。
WeatherData对象跟物理气象站的联系程序已拥有,WeatherData对象拿到数据后会更新到三个布告板上,显示的内容分别是:目前状况、气象统计、天气预报。
我们看下WeatherData的结构(伪代码):
class WeatherData
{
/*下面这三个方法各自返回最近的气象数据
*分别为温度、湿度、气压,我们不在乎它怎么获得的
*只需要知道它能从气象站获取更新
*/
getTemperature();//温度
getHumidity();//湿度
getPressure();//气压
//我们要实现的
measurementsChanged()
{
//一旦气象测量数据更新 这个方法就会被调用
}
}
我们的工作就是实现measurementsChanged()方法,好让它更新目前的状况、气象统计、天气预报三块板子。
再总结下我们手中的条件:
1.WeatherData类具有getter方法,可以获取三个值:温度、湿度、气压。
2.当有新数据时,measurementsChanged()方法就会被调用(我们不在乎它是如何被调用,只需知道它被调用了)。
3.需要实现三个使用天气数据的布告板:目前状况、气象统计、天气预报。一旦有新数据必须马上更新。
4.此系统满足可拓展,可以让开发人员定制布告板,随意的添加、删除布告板。
先看一个错误🙅示范
我们在measurementsChanged()方法中添加我们的代码:
public class WeatherData
{
//实例变量声明省略...
public void measurementsChanged()
{
//先调用本类的三个getXXX方法取得最新数据,set方法默认已实现
float temp = gettemperature();
float humidity = getHumidity();
float pressure = getPressure();
//现在传入数据更新布告板 有三块布告板顺序更新
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
//一些其它方法...
}
想想这有什么不对吗
我们上一篇中将策略模式时提到,要封装变化的部分,与不变者隔离,上面实现的代码很明显针对具体实现编程,导致以后如果删除、增加布告板都要改这部分代码,侵入了这个类。
从三个布告板的更新方法都是xxx.update()方法来看,好像可以统一接口?
在继续之前我们先来看下报纸和杂志的订阅:
1.报社的业务就是出版报纸
2.你向某家报社订阅报纸,只要出新报纸,就会给你发报。
3.当你取消订阅,不交money后,他不就不会再给你发了。
4.只要报社还在运营,就一直会有人订阅或者取消订阅。
那么:出版者 + 订阅者 = 观察者模式
出版者我们改为“主题”(Subject),订阅者我们叫观察者(Observe)。
我们再形象一点,课堂上,老师对学生的讲课:
我们把教师看作主题(Subject),当老师获取到教案上新的内容时,用嘴巴讲出来(更新数据),下面的学生可以看作一群观察者,如果有不听讲睡大觉的😴或请假了的,我们可以看作该观察者暂时取消订阅该主题😄。如图:
我们可以看出观察者模式主要就是一对多的关系。
定义观察者模式的类图:
classDiagram
class Observer{
<<interface>>
+所有潜在的观察者必须实现观察者接口
+update()
}
Observer <|-- Subject
Observer <|.. ConcreteObserver
class Subject
<<Interface>> Subject
Subject: +//每个主题可以有许多观察者
Subject : +注册观察者registerObserver()
Subject : +移除观察者removeObserver()
Subject: +通知所有观察者notifyObserver()
Subject <|.. ConcreteSubject
class ConcreteSubject{
+//具体主题总是实现主题接口
+注册观察者registerObserver()
+移除观察者removeObserver()
+取消订阅notifyObserver()
+获取状态getState()
+设置状态setState()
}
ConcreteSubject <|-- ConcreteObserver
class ConcreteObserver{
//其它观察者的具体类型
//必须注册主题以便接收通知
+update()
}
从图中我们可以看到,观察者是主题的依赖者,在数据变化时,通知观察者,这样比把观察者一股脑的去控制数据好多了。
松耦合的威力
当两个对象之间松耦合,它们仍然可以交互,但是不太清楚彼此的细节。观察者模式提供了这种设计让其松耦合。
为什么呢
任何时候,我们可以增添新的观察者,且主题并不需要改变代码,设计变得更加具有复用与弹性。
设计原则4
为了交互对象之间的松耦合设计而努力!
(另外设计原则1到3且看上篇博文)
重新回到气象站应用
我们发现气象站应用很符合用观察者模式,weatherData正是主题,多个布告板就是观察者,布告板需要向weatherData注册,并且自身要有update()方法供weatherData调用更新数据。
由于每个布告板都有些许不同,所以update方法应该定义在布告板共同的接口中。
设计气象站
设计图:
根据我们的设计图,我们开始实现气象站:
public interface Subject
{
//注册与移除观察者都需要一个观察者作为变量Observer
public void registerObserver(Observer o);
public void removeObserver(Observer o);
//刚主题改变时 这个方法会被调用以通知观察者
public void notifyObserver(Observer o);
}
public interface Observer
{
//所有观察者都需实现的update方法
//这里是把温湿度等传进来,想想还有别的什么封装方法(对象)
public void update(float temp, float humidity, float pressure);
}
public interface DisplaElement
{
//当布告板需要显示时调用此方法
public void display();
}
编写一下weatherData,省略了package、import等语句
public class WeatherData implements Subject
{
//ArrayList用来保存观察者
private ArrayList<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
//在构造方法中初始化了observers
public WeatherData()
{
observers = new ArrayList();
}
public void registerObserver(Observer o)
{
//向集合中注册观察者
observers.add(o);
}
public void removeObserver(Observer o)
{
//观察者取消订阅 就从集合中删除
int index = observers.indexOf(o);
if(index >= 0)
{
observers.remove(index);
}
}
public void notifyObserver()
{ //通知所有的观察者(或可以采用并行流)
observers.forEach(item->{
item.update(temperature, humidity, pressure);
})
}
public void measurementsChanged()
{
//当气象站得到最新数据后会调用此方法,通知观察者
notifyObserver();
}
public void setMeasurements(float temp, float humidity, float pressure)
{
//使用此方法set观测数据 模拟从气象站获得数据后的自动调用measurementsChanged方法
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
//一些其它方法......
}
weatherData已经写出来了,我们接着再把布告板程序写出来,一共三个布告板:目前情况、统计情况、天气预测。
public class CurrentConditionsDisplay implements Observer, DisplayElement
{
private float temperature;
private float humidity;
private Subject weatherData;
//构造器需要weatherData 订阅主题(注册)
public CurrentConditionsDisplay(Subject weatherData )
{
this.weatherData = weatherData;
//将当前对象注册到主题
weatherData.registerObserver(this);
}
public void update(float temp, float humidity, float pressure)
{
this.temperature = temp;
this.humidity = humidity;
//显示
display();
}
//显示结果
public void display()
{
System.out.println("Current conditions: "+ temperature + "F degress and "+ humidity + "% humidity");
}
}
建立一个测试程序:
public class WeatherStation
{
public static void mian(String[]args]
{
//首先建立一个WeatherData
WeatherData weatherData = new WeatherData();
//先建立一个布告板 其余同理
CurrentConditionsDisplay = currentDisplay = new CurrentConditionsDisplay(weatherData);
weatherData.setMeasurements(80,65,30.4f);
}
}
其它的布告板大家可以自行试一下。
其实Java中就内置了这种观察者模式,但跟我们设计的稍有差别,比如通知观察者时,可能有些信息是冗余的,在Java自带的类库中,可以选择通知那些内容给观察者。
你可能会注意到有一个setChange方法,是用来标记状态已经改变的事实,好让notify Observers方法知道它被调用时应该更新观察者,反之,如果在notifyObservers方法之前没有调用setChange,则不会通知观察者。
利用内置的这些支持,我们重做一下啊气象站的应用。
import java.util.Observer;
import java.util.Observabel;
public class weatherData extends Observable
{
private float temperature;
private float humidity;
private float pressure;
//构造方法不需要用来注册观察者了
public WeatherData(){}
public void measurementsChanged()
{
setChanged();//确认状态改变
//没有传送数据对象,说明采用拉取数据方式
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
//省略一些get、set方法
}
重做CurrentConditionsDisplay
public class CurrentConditionsDisplay implements Observer,DisplayElement
{
private float temperature;
private float humidity;
private Observer observer;
pubic CurrentCondition(Observer observer)
{
this.observer = observer;
observer.addObserver(this);
}
public void update(Observable, obs, Object arg)
{ //传入了Observable 与 数据对象Object
if(obs instanceof WeatherData)
{
WeatherData weatherData = (WeatherData)obs;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
else
{
System.out.println("需要传入合适的对象属于WeatherData子类");
}
}
public void display()
{
System.out.println("Current conditions : "+ temperature + " F degress and "+ humidity + "% humidity");
}
}
不知道大家有没有发现,两种类似设计输出效果顺序不一致。
我们 不能依赖观察者通知的顺序,这种设计违背了一个原则“面向接口编程,而不是实现。”
因为Observable是一个类,有些时候会陷入两难,因为Java无法通过类来多重继承。
在Java当中还有哪些地方可以用到观察者模式,我们再看一个例子,很简单,假如你有一个摁钮,上面写着“Should I do it?”(我该做吗)。
当你摁下这个摁钮,倾听者(观察者)必须回答此问题,我们实现了两个倾听者,一个是天使,一个是恶魔。程序的行为如下:
来看下代码。用到了Java中的GUI组件,在当今Java的使用环境,它的GUI组件基本没什么企业在用了,看官们大体知道一些即可。
//天使
class AngelListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
//不要这样做,你可能会后悔。
System.out.println("Don't do it, you might regret it");
}
}
//恶魔
class DevilListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("Come on, do it!");
}
}
public class SwingObserverExample
{
JFrame frame;
public static void main(String[]args)
{
SwingObserverExample example = new SwingObserverExample();
example.go();
}
public void go()
{
JButton button = new JButton("Should I do it?");
//制造两个监听者
button.addActionListener(new AngelListener());
button.addActionListener(new DevilListener());
setFrame(button);
}
public void setFrame(JButton button)
{
frame = new JFrame();
//添加至frame容器
frame.getContentPane().add(BorderLayout.CENTER, button);
//点击X退出否则在后台运行
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//窗体大小
frame.setSize(800,500);
//位置 中间
frame.setLocationRelativeTo(null);
//设置窗体可见,否则不显示
frame.setVisible(true);
}
}
Java中自带的类库使用观察者模式的有很多,如javaBeans,RMI,包括后来推出的MVC模式,也使用了观察者模式的思想。
记住咱们的新设计原则:
为交互对象之间的松耦合设计而努力。
再次重申一遍正式的定义:观察者模式 在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。
就到这里吧,下次再一起回顾一下装饰者模式。
本文部分图片与内容引自《head first 设计模式》 <未完待续>