flyEn'blog

设计模式(二)观察者模式

本文为看《Head First设计模式》书后整理的技术分享第二篇。

并结合发布订阅模式作比较和总结。

观察者模式

这个模式可以帮你的对象知悉现况,不会错过该对象感兴趣的事。对象甚至在运行时可决定是否要继续被通知。

设计原则

  • 为了交互对象之间松耦合设计而努力。

举例

书中以一个例子来展开观察者模式。

👉Internet气象观测站:一个WeatherData对象负责追踪目前的天气状况(温度、湿度、气压)。我们希望建立一个应用,有三种布告板,分别显示目前的状况、气象统计及简单的预报。当WeatherObject对象获得最新的测量数据时,三种布告板必须实时更新。

而且,这是一个可以扩展的气象站,Weather-O-Rama气象站希望公布一组API,好让其他开发人员可以写出自己的气象布告板,并插入此应用中。

气象监测应用的概况

此系统中的三个部分是气象站(获取实际气象数据的物理装置)、WeatherData对象(汇总来自气象站的数据)和布告板(目前状态、气象统计、天气预报等)。

我们看一看刚开始WeatherData初步设想到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class WeatherData{
public int getTemperature(){
}
public int getHumidity(){
}
public int getPressure(){
}
public void measurementsChanged(){
//一旦气象测量更新,此方法会被调用
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
// 这里是其他WeatherData方法
}

由上面这个设计会出现的以下几个问题

1
2
3
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);

这三个方法针对具体实现编程,不针对接口编程,会导致我们以后再增加或删除布告板必须修改WeatherData类。

认识观察者模式

我们先来看观察者模式,然后再回来看如何将此模式应用到气象观察站。

我们看看报纸和杂志的订阅是怎么回事。

  1. 报社的业务就是出版报纸;
  2. 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来,只要你是他们的订订户,你就会一直收到报纸。
  3. 当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
  4. 只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。

其实观察者模式可以理解成 出版者+订阅者=观察者模式

如果你了解了报纸的订阅,其实就知道观察者模式是怎么回事了,只是名称不太一样:出版者改为“主题”,订阅者改为“观察者”。

观察者模式 定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

定义观察者模式:类图

松耦合的威力

当两个对象之间松耦合,他们依然可以互通,但是不太清楚彼此的细节。

观察者模式提供了一种对象设计,让主题和观察者之间松耦合。

关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口),主题不需要知道观察者的具体类是谁,做了什么或其他任何细节。任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是实现Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们甚至可以用新的观察者代替现用的观察者,主题不会受到任何影响。同样的,也可以随时删除某主题的一些观察者。

有新类型的观察者出现时,主题的代码不需修改,假如我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题代码。所以要做的就只是在新类里实现观察者接口,之后注册为观察者即可。

🌟第四个设计原则:为了交互对象之间松耦合设计而努力

回过头我们可以重新设计气象站了。

image-20200709152429928

实现气象站代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//主题接口
interface Subject{
//注册观察者
public void registerObserver(Observer o);
//删除观察者
public void removeObserver(Observer o);
//当主题状态改变时,这个方法会被调用,以通知所有的观察者
public void notifyObserver();
}

interface Observer {
//当气象观测值改变时,主题会把这些状态值当作方法的参数,传送给观察者
public void update(float temp,float humidity,float pressure);
}

interface DisplayElement{
//当布告板需要显示时,调用此方法
public void display();
}

在WeatherData中实现主题接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class WeatherData implements Subject{

private ArrayList<Observer> observers;
private float temperature;
private float humidity;
private float pressure;

public WeatherData(){
observers=new ArrayList<Observer>();
}

@Override
public void registerObserver(Observer o) {
observers.add(o);
}

@Override
public void removeObserver(Observer o) {
int i=observers.indexOf(o);
if(i>=0){
observers.remove(i);
}
}

@Override
public void notifyObserver() {
for(Observer observer:observers){
observer.update(temperature,humidity,pressure);
}
}
//当从气象站得到更新观测值时,我们通知观察者
public void measurementsChanged(){
notifyObserver();
}

public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
measurementsChanged();
}
//WeatherData的其他方法
}

建立布告板

其中一个布告板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CurrentConditionDisplay implements Observer,DisplayElement{
private float temperature;
private float humidity;
private Subject weatherData;

public CurrentConditionDisplay(Subject weatherData){
this.weatherData=weatherData;
weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("数据");
}

@Override
public void update(float temp, float humidity, float pressure) {
this.temperature=temp;
this.humidity=humidity;
display();
}
}

使用Java中内置的观察者模式

在java api有内置的观察者模式。java.util包内包含最基本的Observer接口和Observable类。

img

Java内置的观察者模式如何运作

WeatherData现在扩展自Observable类,并继承到一些增加、删除、通知观察者的方法。

如何把对象变成观察者

实现观察者接口(Java.util.Observer),如何调用任何Observable对象的addObserver()方法,不想再当观察者时,调用deleteObserver()方法即可。

重写WeatherData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class WeatherDataTWO extends Observable{
private float temperature;
private float humidity;
private float pressure;

public WeatherDataTWO(){

}

public void measurementsChanged(){
//在调用notifyObservers()之前,要先调用setChanged()来指示状态已经改变
setChanged();
//我们没有调用notifyObservers传送数据对象,表示我们采用的做法是拉。
notifyObservers();
}

public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
measurementsChanged();
}

public float getTemperature() {
return temperature;
}

public float getHumidity() {
return humidity;
}

public float getPressure() {
return pressure;
}
}

可观察者要如何送出通知:

  1. 先调用setChanged()方法,标记状态已经改变的事实。(可有更多的弹性,可以控制推送更新的频率)

  2. 然后调用两种notifyObservers()方法中的一个 notifyObservers()(pull)、notifyObservers(Object arg)(push)

重写布告板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CurrentConditionsDisplay implements java.util.Observer,DisplayElement{

Observable observable;
private float temperature;
private float humidity;

public CurrentConditionsDisplay(Observable observable){
this.observable=observable;
observable.addObserver(this);
}

@Override
public void display() {
System.out.println("数据");
}

@Override
public void update(Observable o, Object arg) {
if(o instanceof WeatherDataTWO){
WeatherDataTWO weatherDataTWO= (WeatherDataTWO) o;
this.temperature=weatherDataTWO.getTemperature();
this.humidity=weatherDataTWO.getHumidity();
display();
}
}
}

Java内置观察者的局限:

Observable是一个类而不是一个接口,你必须设计一个类来继承它。如果某个类想同时具有Observable类和另一个超类的行为时就完了,java不支持多重继承。所以有时候需要自己实现一整套观察者模式。

总结定义

观察者模式 定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

观察者模式和发布订阅模式

区别

我之前以为发布订阅模式里的Publisher就是观察者模式里的Subject,而Subscriber就是Observer。Publisher变化时,就主动去通知Subscriber。

**在发布订阅模式里,发布者并不会直接通知订阅者,发布者和订阅者互不感知。

他们通过第三者,也就是经纪人(Broker)来实现调度的。

image-20200709164555084

发布者只需要告诉Broker,我要发消息,注册topic是AAA;订阅者只需告诉Broker,我要订阅topic是AAA的消息;

于是,当Broker收到发布者发来消息,并且topic是AAA时,就会把消息推给订阅了topic是AAA的订阅者。

也就是说,发布订阅模式里,发布者和订阅者,不是松耦合,而是完全解耦的。

发布订阅模式简单代码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PubSub {
PubSub() {
this.subscribers = {}
}
subscribe(type, fn) {
if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) {
this.subscribers[type] = [];
}

this.subscribers[type].push(fn);
}
unsubscribe(type, fn) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
this.subscribers[type] = listeners.filter(v => v !== fn);
}
publish(type, ...args) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
listeners.forEach(fn => fn(...args));
}
}

let ob = new PubSub();
ob.subscribe('add', (val) => console.log(val));
ob.publish('add', 1);

由上面代码可以看出,发布订阅模式统一由调度中心处理,消除了发布者和订阅者之间的依赖。

总结

从表面上看:

  • 观察者模式里,只有两个角色 —— 观察者 + 被观察者
  • 而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经纪人Broker

从更深层次看:

  • 观察者和被观察者,是松耦合的关系
  • 发布者和订阅者,则完全解耦

从使用层面看:

  • 观察者模式,多用于单个应用内部;
  • 发布订阅模式,则更多的是一种跨应用的模式,比如我们常用的消息中间件。

应用场景

比如按钮监听、vue的数据双向绑定、kafka消息队列、Redis、zookeeper…

Fork me on GitHub