一、观察者模式:从“盯梢”到“通知”的思维转变
观察者模式,说白了就是解决“一个对象状态变了,其他一堆对象怎么知道”的问题。
我刚开始接触这个模式时,觉得它不就是个“广播”嘛。后来在项目中踩过坑才明白,它背后藏着更深的软件设计哲学——发布-订阅模型和松耦合思想。
今天咱们就把它拆开揉碎了讲清楚。
1.1 观察者模式的定义
官方定义是这样的:定义对象之间的一对多依赖关系,当一个对象状态发生变化时,所有依赖它的对象都会得到通知并自动更新。
嗯,听起来有点绕。我换个说法:
- 被观察者(Subject):就是那个“被盯着的对象”,它状态变了要喊一嗓子。
- 观察者(Observer):就是那些“盯着别人”的对象,收到通知后做自己的事。
举个例子。你在写一个天气预报系统。天气数据变了(温度、湿度),UI界面要刷新,数据库要存储,报警系统要判断是否发预警。如果每个模块都去轮询天气数据,那代码就乱成一锅粥了。
观察者模式的做法是:天气数据作为被观察者,UI、数据库、报警系统都注册成观察者。天气一变,自动通知它们。
核心思想:被观察者不关心谁在观察自己,观察者也不依赖被观察者的具体实现。它们只通过一个“通知接口”通信。
1.2 发布-订阅模型:观察者模式的“升级版”
观察者模式有两种常见实现方式:
- 经典观察者模式:观察者直接注册到被观察者上。被观察者维护一个观察者列表。
- 发布-订阅模型:中间加一个“事件通道”或“消息代理”。发布者和订阅者不直接接触。
我个人习惯把发布-订阅模型看作是观察者模式的“解耦加强版”。
为什么会这样?你想想看,经典模式里,被观察者还得维护一个观察者列表。如果观察者多了,或者观察者之间还有依赖关系,维护起来就头疼了。
发布-订阅模型引入了一个中间层。发布者只管发消息,订阅者只管收消息。中间层负责路由和分发。这样,发布者和订阅者完全不知道对方的存在。
我在项目中遇到过这样一个场景:一个电商系统,用户下单后,需要通知库存系统扣库存、通知物流系统生成运单、通知积分系统加积分、通知短信系统发通知。如果用经典观察者模式,订单服务要维护四个观察者。后来改成发布-订阅模型,订单服务只管往消息队列里丢一条“订单创建”事件,其他系统自己去订阅。清爽多了。
我的建议:如果观察者数量固定且变化少,用经典模式就够了。如果观察者可能动态增减,或者系统需要跨进程通信,果断上发布-订阅模型。
1.3 松耦合设计思想:观察者模式的灵魂
观察者模式最牛的地方,不是它怎么通知,而是它怎么不通知。
什么意思?
松耦合的核心是:被观察者不需要知道观察者的任何细节。它只知道观察者实现了某个接口(比如叫 update)。至于观察者收到通知后是刷新UI、写数据库、还是发邮件,被观察者一概不管。
这带来的好处很明显:
- 可扩展性:加一个新观察者,不需要改被观察者的代码。写个新类,注册进去就行。
- 可维护性:观察者出问题了,不会影响被观察者和其他观察者。
- 可复用性:被观察者可以在不同场景下复用,只要观察者接口不变。
我曾经踩过一个坑:在一个监控系统中,被观察者直接调用了观察者的具体方法。后来要加一个新的报警方式,得改被观察者的代码。改完发现,原来那个观察者也被影响了。嗯,从那以后,我再也不敢让被观察者直接依赖具体观察者了。
注意:松耦合不是“无耦合”。观察者和被观察者之间仍然存在接口依赖。这个接口要设计得足够稳定,否则一改接口,两边都得改。
1.4 代码示例:一个简单的观察者模式
咱们用Java写个最简单的例子。天气数据作为被观察者,两个观察者:显示面板和报警系统。
// 观察者接口
interface Observer {
void update(float temperature, float humidity);
}
// 被观察者接口
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// 具体被观察者:天气数据
class WeatherData implements Subject {
private List<Observer> observers = new ArrayList<>();
private float temperature;
private float humidity;
public void setMeasurements(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
notifyObservers(); // 数据变了,通知大家
}
@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);
}
}
}
// 具体观察者:显示面板
class DisplayPanel implements Observer {
@Override
public void update(float temperature, float humidity) {
System.out.println("显示面板更新:温度=" + temperature + ", 湿度=" + humidity);
}
}
// 具体观察者:报警系统
class AlarmSystem implements Observer {
@Override
public void update(float temperature, float humidity) {
if (temperature > 40) {
System.out.println("报警:温度过高!");
}
}
}
// 使用
public class Main {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
DisplayPanel panel = new DisplayPanel();
AlarmSystem alarm = new AlarmSystem();
weatherData.registerObserver(panel);
weatherData.registerObserver(alarm);
weatherData.setMeasurements(38.5f, 65.0f);
weatherData.setMeasurements(42.0f, 70.0f); // 触发报警
}
}
你看,WeatherData 只调用了 Observer 接口的 update 方法。它不知道 DisplayPanel 和 AlarmSystem 具体干了什么。这就是松耦合。
1.5 观察者模式的知识体系
下面这张图帮你理清观察者模式的核心逻辑:
1.6 观察者模式 vs 发布-订阅模型
我把它们的区别整理成一张表,方便你对比:
| 对比维度 | 经典观察者模式 | 发布-订阅模型 |
|---|---|---|
| 通信方式 | 观察者直接注册到被观察者 | 通过中间事件通道通信 |
| 耦合度 | 观察者与被观察者仍有接口依赖 | 发布者和订阅者完全解耦 |
| 适用场景 | 单进程、观察者数量固定 | 跨进程、观察者动态增减 |
| 实现复杂度 | 低,直接维护列表 | 高,需要消息代理 |
| 典型应用 | GUI事件监听、MVC架构 | 消息队列、事件驱动架构 |
1.7 避坑指南:我踩过的三个坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
- 坑一:观察者通知顺序依赖。我曾经以为观察者收到通知的顺序是固定的,结果在代码里写了顺序依赖。后来换了个JDK版本,顺序变了,程序就崩了。记住:永远不要依赖观察者的通知顺序。
- 坑二:观察者内部抛异常。一个观察者update方法里抛了异常,导致其他观察者收不到通知。后来我加了try-catch,确保一个观察者出问题不影响其他人。
- 坑三:忘记注销观察者。在Web应用里,用户登录后注册了观察者,退出时没注销。结果观察者列表越来越大,内存泄漏了。嗯,从那以后,我每次注册观察者都会在finally块里确保注销。
观察者模式看起来简单,但用好了能让你的系统灵活很多。关键是理解它背后的松耦合思想——让对象之间只通过接口通信,不依赖具体实现。这样,你的代码才能经得起变化。
公众号:蓝海资料掘金营,微信deep3321