观察者模式(Observer)
定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
初识
我们先来了解一下报纸和杂志的订阅是怎么回事:
- 报社的业务就是出版报纸、杂志等各种出版物。
- 如果我想看报社的 A 报纸和 B 杂志,那么就向报社订阅 A 报纸和 B 杂志。
- 当他们有新的 A 报纸或 B 杂志出版时,就会向你派送,只要你是他们的订户,你就会一直收到新报纸,新杂志。
- 如果你不想看 B 杂志了,取消订阅,他们就不会再送新的 B 杂志给你了。但不会影响你订阅的 A 报纸。
- 只要报社还在运营,就会一直有人向他们订阅或取消报纸等出版物。
在观察者模式中,出版者报社 = 主题(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); }
|
有什么问题呢?
- 如果有添加和删除布告板的需求,那么就必须改动这些代码,不利于项目的扩展。(想一想每次都要修改、编译、打包就觉得累)
- 这些布告板都有一个 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() { setChanged(); 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(); } }
|
布告板实现 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) { 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 类来获得可被观察的行为的,这违背了设计原则的“多用组合,少用继承”。