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 图来展示观察者模式的结构,以及它如何映射到开闭原则和迪米特法则。

观察者模式与设计原则关系图 Subject(主题接口) registerObserver() removeObserver() notifyObservers() Observer(观察者接口) update() 依赖抽象接口 ConcreteSubject - observers: List<Observer> + setState() ObserverA + update() ObserverB + update() 开闭原则(OCP) 新增观察者时,只需实现 Observer 接口 无需修改 Subject 或 ConcreteSubject 迪米特法则(LoD) 主题只依赖 Observer 接口 不知道具体观察者的任何内部细节

四、避坑指南:我踩过的两个坑

理论说完了,聊聊实战中容易翻车的地方。

坑一:观察者太多,通知性能下降

我曾经在一个项目中,给一个主题注册了上百个观察者。每次状态变更,都要遍历整个列表逐个通知。结果在高峰期,一次通知耗时几十毫秒,直接拖慢了主流程。

建议:如果观察者数量很大,考虑使用异步通知(比如消息队列),或者对观察者做分组、优先级排序。别让通知逻辑阻塞主线程。

坑二:观察者之间产生循环依赖

有一次,观察者A在 update 方法里修改了主题的状态,导致主题再次通知所有观察者,观察者B又修改了状态……最后形成了死循环。我当时 debug 到凌晨两点才找到原因。

建议:在 update 方法中尽量避免修改主题状态。如果确实需要,加一个标志位或版本号来防止重复通知。或者干脆把「修改状态」和「通知观察者」拆成两个独立步骤。

五、总结

观察者模式不是银弹,但它确实在「解耦」这件事上做得非常漂亮。它通过抽象接口,让主题和观察者之间只保持最少的联系——主题只知道观察者能 update,观察者只知道主题会通知。这种设计,恰好踩中了开闭原则和迪米特法则的点。

我个人习惯在以下场景优先考虑观察者模式:

  • 事件驱动系统:比如 UI 中的按钮点击、状态变更
  • 数据同步:一个数据源变化,多个展示模块需要更新
  • 消息通知:系统内部的事件广播

你想想看,如果你的代码里到处都是 A 直接调用 B、B 直接调用 C,那改起来得多痛苦?观察者模式就是帮你把这些「直接调用」变成「间接通知」,让每个模块都只关心自己的事。

嗯,今天就聊到这。记住一句话:好的设计,不是让代码跑起来,而是让代码改起来不痛苦。

小提示:观察者模式在 Java 中已经有现成的实现——java.util.Observer 和 java.util.Observable。不过 Java 9 之后标记为废弃,推荐用 PropertyChangeListener 或者自己手写。我个人更倾向于手写,因为更灵活,也更容易控制。

公众号:蓝海资料掘金营,微信deep3321