18. 观察者模式:定义、结构、实现(推模型、拉模型)、优缺点、应用场景
观察者模式,说白了就是「发布-订阅」机制。一个对象状态变了,其他依赖它的对象能自动收到通知。我刚开始接触设计模式时,觉得这玩意儿不就是事件监听吗?后来在项目中踩过坑才明白——它远不止这么简单。
18.1 定义与核心思想
观察者模式定义了一种一对多的依赖关系。当被观察者(Subject)状态发生变化时,所有依赖它的观察者(Observer)都会收到通知并自动更新。
嗯,这里要注意:观察者模式强调的是「松耦合」。被观察者不需要知道观察者的具体实现,只需要知道它们实现了同一个接口。我在一个电商项目中就用到了这个特性——订单状态变了,库存系统、物流系统、通知系统各自做自己的事,互不干扰。
18.2 结构组成
观察者模式的结构其实很清晰,就四个角色:
| 角色 | 职责 | 我见过的坑 |
|---|---|---|
| Subject(目标) | 维护观察者列表,提供注册/删除方法 | 很多人忘记考虑线程安全问题 |
| ConcreteSubject(具体目标) | 状态变化时通知所有观察者 | 通知顺序依赖导致的问题 |
| Observer(观察者) | 定义更新接口 | 接口设计太粗或太细 |
| ConcreteObserver(具体观察者) | 实现更新逻辑 | 更新逻辑里又触发了新的通知 |
核心要点:被观察者只依赖观察者接口,不依赖具体实现。这是依赖倒置原则的典型应用。
18.3 推模型 vs 拉模型
这是观察者模式里最容易让人纠结的地方。我当年也困惑过——到底该把数据推给观察者,还是让观察者自己来拉?
推模型(Push Model)
被观察者主动把详细信息推给观察者。说白了就是「我给你,你接着」。
// 推模型:被观察者把数据作为参数传过去
public interface Observer {
void update(String temperature, String humidity, String pressure);
}
public class WeatherStation implements Subject {
private String temperature;
private String humidity;
private String pressure;
public void notifyObservers() {
for (Observer observer : observers) {
// 直接把三个数据都推过去
observer.update(temperature, humidity, pressure);
}
}
}
优点:观察者拿到数据就能用,不用再查询。响应快。
缺点:如果数据项很多,接口参数会爆炸。而且有些观察者可能只需要其中一部分数据,造成了浪费。
我的经验:推模型适合数据量小、所有观察者都需要全部数据的场景。比如股票行情推送,每个客户端都需要最新的价格和成交量。
拉模型(Pull Model)
被观察者只通知「我变了」,具体数据由观察者自己来取。说白了就是「我告诉你变了,你自己来看」。
// 拉模型:只传递被观察者自身引用
public interface Observer {
void update(Subject subject);
}
public class WeatherStation implements Subject {
private String temperature;
private String humidity;
private String pressure;
public void notifyObservers() {
for (Observer observer : observers) {
// 只传自己,观察者按需获取
observer.update(this);
}
}
// 观察者通过 getter 按需获取
public String getTemperature() { return temperature; }
public String getHumidity() { return humidity; }
public String getPressure() { return pressure; }
}
public class PhoneDisplay implements Observer {
public void update(Subject subject) {
WeatherStation ws = (WeatherStation) subject;
// 我只关心温度,只拉取温度
this.temperature = ws.getTemperature();
}
}
优点:接口稳定,观察者按需获取,灵活度高。
缺点:观察者需要知道被观察者的具体类型(需要向下转型),耦合度略高。
避坑指南:我曾经在一个项目中用了拉模型,结果观察者每次更新都去数据库查数据,导致数据库压力暴增。后来加了缓存才解决。拉模型要注意——别让观察者做太重的查询操作。
18.4 完整实现示例
我习惯用推模型实现一个简单的天气站。你想想看,这样更直观:
// 观察者接口
public interface Observer {
void update(float temperature, float humidity, float pressure);
}
// 被观察者接口
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// 具体被观察者
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
observers.remove(o);
}
public void notifyObservers() {
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();
}
}
// 具体观察者
public class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
public void display() {
System.out.println("当前温度: " + temperature + "°C");
System.out.println("当前湿度: " + humidity + "%");
}
}
18.5 优缺点分析
| 优点 | 缺点 |
|---|---|
| 松耦合:被观察者和观察者之间只依赖抽象接口 | 通知顺序不可控:多个观察者时,更新顺序不确定 |
| 支持广播通信:一对多通知,效率高 | 可能引起循环依赖:观察者更新又触发被观察者变化 |
| 符合开闭原则:新增观察者无需修改被观察者 | 性能问题:观察者太多时通知开销大 |
| 运行时动态建立订阅关系 | 内存泄漏风险:忘记注销观察者会导致对象无法回收 |
我曾经踩过的坑:在一个Android项目中,Activity作为观察者注册到全局的EventBus,但Activity销毁时忘记注销。结果Activity对象一直被持有,导致内存泄漏。后来我强制要求所有观察者必须在onDestroy中注销,才解决了这个问题。
18.6 应用场景
观察者模式在实际项目中太常见了。我总结了几类典型场景:
- 事件驱动系统:GUI框架中的按钮点击事件、鼠标移动事件。Java的Swing、Android的OnClickListener都是观察者模式。
- 消息队列/发布订阅:Kafka、RabbitMQ本质上就是观察者模式的分布式版本。生产者发布消息,消费者订阅消息。
- 数据同步:一个数据源变化,多个视图需要同步更新。比如Excel表格中,修改一个单元格,所有引用该单元格的公式自动重算。
- 状态监控:服务器状态变化时,监控系统、告警系统、日志系统各自响应。
- 响应式编程:RxJava、Reactor等框架的核心思想就是观察者模式。
我的建议:如果你发现代码中需要频繁轮询来获取状态变化,或者多个模块需要响应同一个事件,那就该考虑观察者模式了。但别滥用——如果只有一两个观察者,直接用回调函数更简单。
18.7 知识体系图
18.8 总结
观察者模式是我用得最多的设计模式之一。它解决的核心问题就是「当A变了,B、C、D怎么知道?」。推模型和拉模型各有优劣,我个人的习惯是:数据量小、所有观察者都需要时用推模型;数据量大、观察者各取所需时用拉模型。
最后提醒一句:观察者模式虽好,但别滥用。如果只是简单的回调,用匿名内部类或Lambda表达式就够了。模式是工具,不是目的。
公众号:蓝海资料掘金营,微信deep3321