观察者模式

**观察者模式**是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

观察者设计模式

问题

假如你有两种类型的对象: ​ 顾客和 商店 。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。

顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。

访问商店或发送垃圾邮件

前往商店和发送垃圾邮件

另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。

我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。

解决方案

拥有一些值得关注的状态的对象通常被称为*目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者* (subscribers)。

观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 1) 一个用于存储订阅者对象引用的列表成员变量; 2) 几个用于添加或删除该列表中订阅者的公有方法。

订阅机制

订阅机制允许对象订阅事件通知。

现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。

因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。

通知方法

发布者调用订阅者对象中的特定通知方法来通知订阅者。

如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有发布者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。

真实世界类比

杂志和报纸订阅

杂志和报纸订阅。

如果你订阅了一份杂志或报纸, 那就不需要再去报摊查询新出版的刊物了。 出版社 (即应用中的 “发布者”) 会在刊物出版后 (甚至提前) 直接将最新一期寄送至你的邮箱中。

出版社负责维护订阅者列表, 了解订阅者对哪些刊物感兴趣。 当订阅者希望出版社停止寄送新一期的杂志时, 他们可随时从该列表中退出。

一个气象站(WeatherData)负责收集温度、湿度、气压,当这些数据有变化时,需要通知所有注册的显示屏(比如“当前天气显示屏”和“统计显示屏”),让他们更新数据

“大喇叭”:被观察者(Subject)接口

首先,气象站需要有个“大喇叭”功能,能让大家来“订阅”或“取消订阅”,并且能在有新消息时“广播”出去。这就是 Subject 接口

1
2
3
4
5
public interface Subject {
void registerObserver(Observer o); // 注册:你过来听我广播
void removeObserver(Observer o); // 移除:你不想听了就走
void notifyObservers(); // 通知:我广播啦,大家听着!
}

2. “听众”:观察者(Observer)接口

然后,每个想接收气象数据更新的“显示屏”都得是个“听众”。它们需要知道,一旦收到通知,该怎么“更新”自己的显示。这就是 Observer 接口

1
2
3
4
public interface Observer {
// update:气象站通知我了,把最新数据告诉我,我来更新
void update(float temperature, float humidity, float pressure);
}

3. “真气象站”:具体被观察者(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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {
private List<Observer> observers; // 听众名单
private float temperature; // 温度
private float humidity; // 湿度
private float pressure; // 气压

public WeatherData() {
observers = new ArrayList<>(); // 初始化听众名单
}

@Override
public void registerObserver(Observer o) {
observers.add(o); // 听众来了,加入名单
System.out.println(" [气象站] " + o.getClass().getSimpleName() + " 注册成功。");
}

@Override
public void removeObserver(Observer o) {
observers.remove(o); // 听众走了,从名单移除
System.out.println(" [气象站] " + o.getClass().getSimpleName() + " 已取消注册。");
}

@Override
public void notifyObservers() {
System.out.println("\n[气象站] 数据变化!开始通知所有听众...");
for (Observer observer : observers) {
// 遍历名单,挨个通知每个听众最新的数据
observer.update(temperature, humidity, pressure);
}
}

// 气象数据发生变化了!
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
// 数据一变,马上通知所有听众
measurementsChanged();
}

private void measurementsChanged() {
notifyObservers(); // 内部方法,直接调用通知
}
}

“真显示屏”:具体观察者(CurrentConditionsDisplay / StatisticsDisplay)

这些就是具体的“听众”,它们会向气象站“注册”自己,一旦收到通知,就按自己的方式显示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;

public CurrentConditionsDisplay(Subject weatherData) {
weatherData.registerObserver(this); // 创建时就向气象站注册自己
}

@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display(); // 收到数据了,赶紧显示出来
}

public void display() {
System.out.println(" [当前天气] 温度: " + temperature + "F, 湿度: " + humidity + "%");
}
}
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
public class StatisticsDisplay implements Observer {
private float maxTemp = 0.0f;
private float minTemp = 200f;
private float tempSum = 0.0f;
private int numReadings;

public StatisticsDisplay(Subject weatherData) {
weatherData.registerObserver(this); // 创建时向气象站注册自己
}

@Override
public void update(float temperature, float humidity, float pressure) {
// 收到数据了,计算统计信息
tempSum += temperature;
numReadings++;
if (temperature > maxTemp) maxTemp = temperature;
if (temperature < minTemp) minTemp = temperature;
display(); // 显示统计结果
}

public void display() {
System.out.println(" [统计信息] 平均: " + (tempSum / numReadings)
+ "F, 最高: " + maxTemp + "F, 最低: " + minTemp + "F");
}
}

Demo.java: 初始化代码

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
public class WeatherStation {
public static void main(String[] args) {
// 1. 建立气象站
WeatherData weatherData = new WeatherData();

// 2. 准备两个显示屏,并让它们“订阅”气象站
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);

// 3. 气象站发布第一次数据
System.out.println("\n--- 第一次气象数据发布 ---");
weatherData.setMeasurements(80, 65, 30.4f);

// 4. 气象站发布第二次数据
System.out.println("\n--- 第二次气象数据发布 ---");
weatherData.setMeasurements(82, 70, 29.2f);

// 5. 统计显示屏不干了,取消订阅
System.out.println("\n--- 统计显示屏取消订阅 ---");
weatherData.removeObserver(statisticsDisplay);

// 6. 气象站发布第三次数据,这时统计显示屏就不会收到通知了
System.out.println("\n--- 第三次气象数据发布 ---");
weatherData.setMeasurements(78, 90, 29.0f);
}
}

观察者模式适合应用场景

当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。

当你使用图形用户界面类时通常会遇到一个问题。 比如, 你创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。

观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。

当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用。

订阅列表是动态的, 因此订阅者可随时加入或离开该列表。

实现方式

  1. 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。

  2. 声明订阅者接口。 该接口至少应声明一个 update方法。

  3. 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。

  4. 确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。

    但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。

  5. 创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。

  6. 在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。

    但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下, 发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。

  7. 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。

观察者模式优缺点

  • 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。

  • 你可以在运行时建立对象之间的联系。

  • 订阅者的通知顺序是随机的。

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。

    • 命令在发送者和请求者之间建立单向连接。

    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。

    • 观察者允许接收者动态地订阅或取消接收请求。

  • 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。

    *中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者*的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。

    有一种流行的中介者模式实现方式依赖于*观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者*非常相似。

    当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和*观察者*并不相同, 但这仍是一种中介者模式。

    假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者