观察者模式
- by Head First 设计模式
在对象之间建立一对多的依赖,这样一来,当一个对象的状态改变,依赖它的对象都会收到通知,并且自动更新。
- by Dive into Design Patterns:
Also Known as: Event-Subscriber, Listener
Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.
设计原则 #
- 找出应用之中可以变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 多用组合,少用继承
- 为交互对象的松耦合设计而努力
- 事实上,不用设计模式也可以硬编码出发布者-订阅者工作模式的代码,只不过发布者与订阅者呆在一起,会比较臃肿😮, 也不利于扩展。
- 在观察者模式中,被观察者(发布者)与观察者(订阅者)是松耦合的,发布者并不关心订阅者的具体细节,只需要知道 其订阅与否,就知道状态变化后是否对其发送通知;同样地,订阅者也不关心发布者如何通知它,只需要处理好自己收到 通知的业务就行了😊
- 松耦合的设计优势得以体现:代码有层次感,易于拓展和维护。
想想看MVC开发模式,这是不是松耦合的设计呢?控制层、模型层、视图层分别有自己的业务范围
UML简图 #
classDiagram direction LR class Publisher { << interface >> + registSubscriber(Subscriber s) + unregistSubscriber(Subscriber s) + notifySubscribers() } Publisher <|.. Client Client *..> Subscriber class Client { - List~Subscriber~ subscribers - Boolean state + registSubscriber(Subscriber s) + unregistSubscriber(Subscriber s) + notifySubscribers() } class Subscriber { << interface >> +update() } Subscriber <|.. ConcreteSubscriber : impl class ConcreteSubscriber { ... + update() }
笔记 #
- 观察者模式定义了对象之间一对多的关系。
- 发布者(被观察者)用一个统一的接口来更新观察者。
- 发布者和订阅者之间使用松耦合(loose-coupling)的方式结合,订阅者不知道观察者的细节,只知道观察者实现观察者接口。
- 使用此模式时,订阅者可以从发布者处"推"或者"拉"数据, 不过"推"一般被认为是正确的方式。
- 有多个订阅者时,可以不依赖特性的通知顺序。
- Java提供了此模式的包,包括
java.util.Observable
(Deprecated since Java 9)。 - 此模式被用在其他地方,如JavaBeans,RMI。
示例代码 #
发布者 #
发布者(Publisher)是一个接口,主要定义了三个方法:
1void register(Subscriber) // 添加订阅
2void unregister(Subscriber) //取消订阅
3void notify() // 发布消息
除了以上的方法外,发布者自然可以添加其他的方法,根据具体业务需求。不过上述3个方法是必须的。
以下是发布者的示例代码,以下示例没有使用所谓的Publisher
和Subscriber
名字,希望你能不通过名字,也能认出它们。
发布者被定义为Subject
,意即主题,正所谓先有”主题“,才可以”订阅“。
1public interface Subject {
2
3 void registerBoard(Board board);
4 void unregisterBoard(Board board);
5 void notifyBoard();
6
7 // other businesses
8}
类WeatherStation
实现了发布者Subject
接口,它定义了一个天气站,用来储存温度、湿度、压力等等天气信息。
1public class WeatherStation implements Subject {
2
3 // 并发风险
4 private List<Board> boards;
5 private boolean status;
6
7 public WeatherStation() {
8 this.boards = new LinkedList<>();
9 this.status = false;
10 }
11
12 @Override
13 public void registerBoard(Board board) {
14 if (!boards.contains(board)) {
15 boards.add(board);
16 }
17 }
18
19 @Override
20 public void unregisterBoard(Board board) {
21 boards.remove(board);
22 }
23
24 @Override
25 public void notifyBoard() {
26 if (status) {
27 for (Board board : boards) {
28 board.update(this);
29 }
30 status = false;
31 }
32 }
33
34 public void setStatus(boolean status) {
35 this.status = status;
36 }
37
38 private float temperature;
39 private float humidity;
40 private float pressure;
41
42 public void setData(float temperature,
43 float humidity,
44 float pressure) {
45 this.temperature = temperature;
46 this.humidity = humidity;
47 this.pressure = pressure;
48 this.status = true;
49 // notifyBoard(); // 可以在此处发出通知
50 }
51
52 public float getTemperature() {
53 return temperature;
54 }
55
56 public float getHumidity() {
57 return humidity;
58 }
59
60 public float getPressure() {
61 return pressure;
62 }
63}
订阅者 #
订阅者(Subscriber)也是一个接口,通常,它只有一个方法,用来更新信息:
1public interface Board {
2 // 观察者收到通知之后的更新,方法参数可以是指定字段或者实体
3 void update(WeatherStation client);
4}
订阅者的实现,就比较简单了。在发布-订阅模型里,订阅者的方法总是发布者主动调用的。
1public class StatisticsBoard implements Board {
2 @Override
3 public void update(WeatherStation client) {
4 System.out.printf("Average weather of this month:" +
5 "\n Average Temperature %.2f celsius" +
6 "\n Average Humidity %.2f" +
7 "\n Average Pressure %.2f\n",
8 client.getTemperature(),
9 client.getHumidity(),
10 client.getPressure());
11 }
12}
客户端 #
1public class WeatherStationClient {
2 public static void main(String[] args) {
3 WeatherStation client = new WeatherStation();
4 client.setData(23.2f, 10.91f, 1.01f);
5 NormalBoard normalBoard = new NormalBoard();
6 client.registerBoard(normalBoard);
7 // register a new listener
8 StatisticsBoard statisticsBoard = new StatisticsBoard();
9 client.registerBoard(statisticsBoard);
10
11 client.notifyBoard(); // 1st notify
12 client.unregisterBoard(normalBoard);
13 client.setStatus(true);
14 client.notifyBoard(); // 2nd notify
15 }
16}
上述示例代码的输出为:
1Today's weather:
2 Temperature 23.20 celsius
3 Humidity 10.91
4 Pressure 1.01
5Average weather of this month:
6 Average Temperature 23.20 celsius
7 Average Humidity 10.91
8 Average Pressure 1.01
9Average weather of this month:
10 Average Temperature 23.20 celsius
11 Average Humidity 10.91
12 Average Pressure 1.01
可以看到,第一次notify
时,2个订阅者都更新了信息。
当移除一个订阅者后,再次notify
,只有一个订阅者更新了信息。
另一个订阅者
NormalBoard
的代码并没给出,很简单,就省略了。