《Head First 设计模式》笔记2

观察者模式(Observer)

定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

初识

我们先来了解一下报纸和杂志的订阅是怎么回事:

  1. 报社的业务就是出版报纸、杂志等各种出版物。
  2. 如果我想看报社的 A 报纸和 B 杂志,那么就向报社订阅 A 报纸和 B 杂志。
  3. 当他们有新的 A 报纸或 B 杂志出版时,就会向你派送,只要你是他们的订户,你就会一直收到新报纸,新杂志。
  4. 如果你不想看 B 杂志了,取消订阅,他们就不会再送新的 B 杂志给你了。但不会影响你订阅的 A 报纸。
  5. 只要报社还在运营,就会一直有人向他们订阅或取消报纸等出版物。

在观察者模式中,出版者报社 = 主题(subject),而我们订阅者 = 观察者(observer)。

栗子

现在有一个系统,包括三部分:

  • 气象站:获取实际气象数据的物理装置。
  • WeatherData 类:追踪来自气象站的数据,并更新布告板(具体怎么追踪的不用管)。
  • 布告板:显示目前的天气状况。

现在的项目是,利用 WeatherData 类取得气象数据,更新三个布告板:目前状况、气象统计和天气预报。

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
class WeatherData {
private float temperature; // 温度
private float humidity; // 湿度
private float pressure; // 气压

public float getTemperature() {
return this.temperature;
}

public float getHumidity() {
return this.humidity;
}

public float getPressure() {
return this.pressure;
}

/**
* 一旦气象数据更新,就会被调用
*/
public void measurementsChanged() {
// 你的代码
}
}

而我们的工作就是实现 measurementsChanged,让它来更新我们的三个布告板(不用知道该方法是如何被调用的,我们只用知道该方法被调用时,我们的布告板也被更新了)。

布告板肯定还会添加或者删除的,所以项目一定要支持扩展。

错误示范

1
2
3
4
5
6
7
8
9
10
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);
}

有什么问题呢?

  1. 如果有添加和删除布告板的需求,那么就必须改动这些代码,不利于项目的扩展。(想一想每次都要修改、编译、打包就觉得累)
  2. 这些布告板都有一个 update 方法,所以这些布告板应该用带有 update 方法的接口或抽象类替代而不是具体实现。

满足需求

一个 WeatherData 类和多个布告板有联系,并且布告板需要 WeatherData 类来通知数据,所以这里应该使用观察者模式。

定义主题接口:

1
2
3
4
5
6
7
8
interface Subject {
// 观察者注册
void registerObserver(Observer o);
// 删除观察者
void removeObserver(Observer o);
// 通知所有观察者
void notifyObservers();
}

观察者接口:

1
2
3
interface Observer {
void update(float temperature, float humidity, float pressure);
}

布告板显示功能:

1
2
3
interface DisplayElement {
void display();
}

然后就是把 WeatherData 类改造成 Subject:

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
44
45
46
47
48
49
50
51
52
53
class WeatherData implements Subject {
private float temperature; // 温度
private float humidity; // 湿度
private float pressure; // 气压

private List<Observer> observers; // 观察者们

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

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

@Override
public void removeObserver(Observer o) {
observers.remove(o);
}

@Override
public void notifyObservers() {
// 通知每一个观察者更新数据
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}

public void measurementsChanged() {
notifyObservers();
}

public float getTemperature() {
return this.temperature;
}

public float getHumidity() {
return this.humidity;
}

public float getPressure() {
return this.pressure;
}

// 模拟数据,方便测试
public void mock(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}

把布告板变成观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class CurrentConditionsDisplay implements Observer, DisplayElement {
private Subject weatherData; // 保存主题,方便之后取消观察
private float temperature;
private float humidity;

public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}

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

@Override
public void display() {
System.out.println("目前状况:" + temperature + " 摄氏度," + humidity + "% 湿度");
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay display1 = new CurrentConditionsDisplay(weatherData);
System.out.println("通知前");
display1.display();
System.out.println("第一次通知后");
weatherData.mock(25, 60, 30.4f);
System.out.println("第二次通知后");
weatherData.mock(20, 72, 41.7f);
}
}

输出:
通知前
目前状况:0.0 摄氏度,0.0% 湿度
第一次通知后
目前状况:25.0 摄氏度,60.0% 湿度
第二次通知后
目前状况:20.0 摄氏度,72.0% 湿度

使用 Java 内置的观察者模式

Java 内置的 Observer 接口和 Observable 类和我们实现的 Subject 接口与 Observer 接口很相似。

这里就将使用这两个内置的接口和类重写上面的天气软件。

WeatherData 类继承 Observable 类:

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
import java.util.Observable;

class WeatherData extends Observable {
private float temperature; // 温度
private float humidity; // 湿度
private float pressure; // 气压

public void measurementsChanged() {
// 指示状态已经改变;如果不指示的话,notifyObservers 无法发出通知
// 详细看源码实现
setChanged();
notifyObservers();
}

// 观察者会利用这些 getter 方法取得 WeatherData 对象的状态
public float getTemperature() {
return this.temperature;
}

public float getHumidity() {
return this.humidity;
}

public float getPressure() {
return this.pressure;
}

// 模拟数据,方便测试
public void mock(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}

布告板实现 Observer 接口:

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
import java.util.Observer;

class CurrentConditionsDisplay implements Observer, DisplayElement {
private Observable observable;
private float temperature;
private float humidity;

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

@Override
public void update(Observable observable, Object arg) {
// 先确定接收的是来自 WeatherData 的,而不是来自其他可观察对象的
if (observable instanceof WeatherData) {
WeatherData weatherData = (WeatherData) observable;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}

@Override
public void display() {
System.out.println("目前状况:" + temperature + " 摄氏度," + humidity + "% 湿度");
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay display1 = new CurrentConditionsDisplay(weatherData);
System.out.println("通知前");
display1.display();
System.out.println("第一次通知后");
weatherData.mock(25, 60, 30.4f);
System.out.println("第二次通知后");
weatherData.mock(20, 72, 41.7f);
}
}

输出结果和上面一致:
通知前
目前状况:0.0 摄氏度,0.0% 湿度
第一次通知后
目前状况:25.0 摄氏度,60.0% 湿度
第二次通知后
目前状况:20.0 摄氏度,72.0% 湿度

如果有多个不同的公告板,上面输出的结果顺序可能会不同,因为 Observable 类通知的先后顺序不依赖于注册的先后。比如 A、B 都订了同一份报纸,并且 A 比 B 先订阅,但派送新报纸时,可能 A 先收到,可能 B 先收到,与注册先后无关,这是松耦合的体现。

注意:WeatherData 类是通过继承 Observable 类来获得可被观察的行为的,这违背了设计原则的“多用组合,少用继承”。

Author

Zoctan

Posted on

2018-03-29

Updated on

2023-03-14

Licensed under