18、观察者模式与设计原则:观察者模式如何体现开闭原则、迪米特法则
聊到观察者模式,很多教程都会说它「实现了松耦合」。但说实话,光听这句话,你很难真正理解它好在哪。我当年刚学设计模式时,也背过这句话,但直到在项目中踩了坑,才真正明白——观察者模式之所以经典,是因为它完美地映射了面向对象设计的两个核心原则:开闭原则和迪米特法则。
今天我们就来拆开看看,观察者模式到底是怎么「伺候」好这两位大佬的。
一、开闭原则:对扩展开放,对修改关闭
开闭原则(Open-Closed Principle, OCP)说白了就是:你写好的代码,别动不动就去改它。新功能应该通过加新代码来实现,而不是改老代码。
观察者模式是怎么做到这一点的?
我们来看一个典型的观察者模式结构:
// 主题接口
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// 观察者接口
public interface Observer {
void update(String event);
}
// 具体主题
public class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private String state;
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
observers.remove(o);
}
public void notifyObservers() {
for (Observer o : observers) {
o.update(state);
}
}
public void setState(String state) {
this.state = state;
notifyObservers();
}
}
// 具体观察者A
public class ObserverA implements Observer {
public void update(String event) {
System.out.println("ObserverA 收到事件: " + event);
}
}
// 具体观察者B
public class ObserverB implements Observer {
public void update(String event) {
System.out.println("ObserverB 收到事件: " + event);
}
}
现在,如果我想加一个新的观察者C,我只需要:
public class ObserverC implements Observer {
public void update(String event) {
System.out.println("ObserverC 收到事件: " + event);
}
}
然后注册一下:
subject.registerObserver(new ObserverC());
你发现了吗?ConcreteSubject 的代码一行都没改。
核心结论:观察者模式通过「主题-观察者」的抽象接口,将「被观察者」与「观察者」解耦。新增观察者时,只需实现 Observer 接口,无需修改 Subject 的任何代码。这就是对扩展开放、对修改关闭的典型体现。
我在项目中遇到过一种情况:业务方突然要求增加一个日志观察者,把所有状态变更记录到数据库。如果当时用的是硬编码调用,我得去改主题类的 notifyObservers 方法,加一行日志代码。但用了观察者模式,我只需要新增一个 LogObserver,注册进去就行。改动范围从「修改核心类」降级为「新增一个类」。这种安全感,你懂的。
二、迪米特法则:最少知识原则
迪米特法则(Law of Demeter, LoD)又叫最少知识原则。它的核心思想是:一个对象应该对其他对象有最少的了解。说白了,别跟陌生人说话。
观察者模式是怎么体现这一点的?
我们来看一个反面例子。假设没有观察者模式,你可能会这样写:
public class WeatherStation {
private DisplayA displayA;
private DisplayB displayB;
private DisplayC displayC;
public void setTemperature(float temp) {
// 直接调用各个显示器的更新方法
displayA.update(temp);
displayB.update(temp);
displayC.update(temp);
}
}
这里的问题很明显:WeatherStation 知道太多东西了。它知道有三个显示器,知道它们各自叫什么类,知道它们都有 update 方法。如果哪天要加一个 DisplayD,WeatherStation 的代码就得改。这就是典型的「高耦合」。
用观察者模式重构后:
public class WeatherStation implements Subject {
private List<Observer> observers = new ArrayList<>();
public void registerObserver(Observer o) {
observers.add(o);
}
public void setTemperature(float temp) {
notifyObservers(temp);
}
private void notifyObservers(float temp) {
for (Observer o : observers) {
o.update(temp);
}
}
}
现在,WeatherStation 只知道它有一堆 Observer,但不知道这些 Observer 具体是谁、有多少个、内部怎么实现的。它只跟 Observer 接口打交道,不跟具体的 DisplayA、DisplayB 打交道。
核心结论:观察者模式通过引入 Observer 接口,让主题只依赖抽象接口,而不是具体类。主题不需要知道观察者的任何细节,只需要知道它们实现了 update 方法。这就是迪米特法则的完美实践——减少对象之间的直接依赖,降低耦合度。
三、一张图看懂:观察者模式如何同时满足两个原则
下面我用一张 SVG 图来展示观察者模式的结构,以及它如何映射到开闭原则和迪米特法则。
四、避坑指南:我踩过的两个坑
理论说完了,聊聊实战中容易翻车的地方。
坑一:观察者太多,通知性能下降
我曾经在一个项目中,给一个主题注册了上百个观察者。每次状态变更,都要遍历整个列表逐个通知。结果在高峰期,一次通知耗时几十毫秒,直接拖慢了主流程。
建议:如果观察者数量很大,考虑使用异步通知(比如消息队列),或者对观察者做分组、优先级排序。别让通知逻辑阻塞主线程。
坑二:观察者之间产生循环依赖
有一次,观察者A在 update 方法里修改了主题的状态,导致主题再次通知所有观察者,观察者B又修改了状态……最后形成了死循环。我当时 debug 到凌晨两点才找到原因。
建议:在 update 方法中尽量避免修改主题状态。如果确实需要,加一个标志位或版本号来防止重复通知。或者干脆把「修改状态」和「通知观察者」拆成两个独立步骤。
五、总结
观察者模式不是银弹,但它确实在「解耦」这件事上做得非常漂亮。它通过抽象接口,让主题和观察者之间只保持最少的联系——主题只知道观察者能 update,观察者只知道主题会通知。这种设计,恰好踩中了开闭原则和迪米特法则的点。
我个人习惯在以下场景优先考虑观察者模式:
- 事件驱动系统:比如 UI 中的按钮点击、状态变更
- 数据同步:一个数据源变化,多个展示模块需要更新
- 消息通知:系统内部的事件广播
你想想看,如果你的代码里到处都是 A 直接调用 B、B 直接调用 C,那改起来得多痛苦?观察者模式就是帮你把这些「直接调用」变成「间接通知」,让每个模块都只关心自己的事。
嗯,今天就聊到这。记住一句话:好的设计,不是让代码跑起来,而是让代码改起来不痛苦。
小提示:观察者模式在 Java 中已经有现成的实现——java.util.Observer 和 java.util.Observable。不过 Java 9 之后标记为废弃,推荐用 PropertyChangeListener 或者自己手写。我个人更倾向于手写,因为更灵活,也更容易控制。