之前写了每日知识-设计模式-状态机模式,今天来复习一种新的模式,观察者模式,我有的时候喜欢管它叫"订阅模式"。
什么是观察者模式?
在社交网站中,一个人关注了另外一个人,比如张三关注了李四,张三更新了动态,就会出现在李四的信息流中。这就是一种观察者模式的场景。
观察者模式是一种行为型设计模式 ,它定义了一种一对多的依赖关系。当一个对象(称为"主题"或"可观察者")的状态发生改变时,所有依赖于它的对象(称为"观察者")都会自动得到通知并被更新。
在设计模式里,具体的实现细节是,观察者把联系方式留给了被观察者,被观察者更新状态的时候,主动推送内容给观察者,观察者被动接受,不用去轮询。观察者通过声明自己观察了被观察者,或者主动向被观察者注册, 实质上留下联系方式,从而当信息更新时第一时间可以获得。
如何实现观察者模式?
观察者模式主要包含四个角色:
- Subject(主题) :维护一个观察者列表,提供添加(
attach
)、删除(detach
)观察者的方法,以及通知所有观察者的方法(notify
)。 - ConcreteSubject(具体主题) :继承自
Subject
。它包含具体的状态数据,当状态改变时,调用父类的通知方法。 - Observer(观察者) :一个接口,通常只定义一个更新方法(
update
),供主题在通知时调用。 - ConcreteObserver(具体观察者) :实现
Observer
接口。它维护一个对ConcreteSubject
的引用,以便在接到通知时,能获取主题的状态并做出相应的反应。
举个例子:气象站监测系统
假设我们有一个气象站(主题),它能监测温度、湿度、气压。当这些数据发生变化时,需要实时更新多个布告板(观察者),比如"当前状况布告板"、"气象统计布告板"。
1. 定义主题接口 (Subject)
java
// 主题接口
public interface Subject {
void registerObserver(Observer o); // 注册观察者(订阅)
void removeObserver(Observer o); // 移除观察者(取消订阅)
void notifyObservers(); // 通知观察者
}
2. 定义观察者接口 (Observer)
java
// 观察者接口
public interface Observer {
void update(float temperature, float humidity, float pressure); // 更新方法
}
3. 实现具体主题 (ConcreteSubject) - 气象站
java
// 具体主题:气象数据
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);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
// 主题遍历所有观察者,调用它们的update方法
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();
}
}
4. 实现具体观察者 (ConcreteObserver) - 布告板
java
// 具体观察者:当前状况布告板
public class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
// 保留对主题的引用,方便以后取消订阅(可选)
private Subject weatherData;
// 在构造时,向主题注册自己(订阅)
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = 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 + "°C 和 " + humidity + "% 湿度");
}
}
5. 测试使用
java
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("模拟新数据...");
weatherData.setMeasurements(26.5f, 65f, 1013.1f);
System.out.println("\n再次模拟新数据...");
weatherData.setMeasurements(28.1f, 70f, 1012.5f);
}
}
输出结果:
erlang
模拟新数据...
当前状况: 26.5°C 和 65.0% 湿度
再次模拟新数据...
当前状况: 28.1°C 和 70.0% 湿度
类之间的关系
通过上面的例子可以看出来:
- 观察者持有被观察者的引用
- 被观察者持有观察者引用的列表
- 被观察者负责通知观察者,需要实现通知方法,提供注册和注销的接口方法,当然还有更新数据的方法,在更新数据的方法内部,需要调用通知方法。
- 观察者提供一个观察者可以被调用的接口方法,用来由被观察者向其推送消息。
双方互相持有对方的引用,被观察者作为1对N的角色,不会特别在意观察者的数量等。
优点与缺点
优点:
- 开闭原则:你可以引入新的观察者类,而无需修改主题代码(对扩展开放)。
- 松耦合:主题只知道观察者实现了某个接口,不知道观察者的具体类是谁、做了什么。它们可以独立变化。
缺点:
- 通知顺序 :观察者的通知顺序是随机的(例如上面用的
ArrayList
),不应依赖特定顺序。 - 性能开销:如果观察者数量非常多,逐个通知会耗时较长。如果更新操作代价高,可能导致性能问题。