首页
/ Java 观察者模式(Observer)深入解析 —— 基于 CS-Notes 设计模式笔记与完整源码实现

Java 观察者模式(Observer)深入解析 —— 基于 CS-Notes 设计模式笔记与完整源码实现

2026-09-06 19:13:18作者:戚魁泉Nursing

观察者(Observer)模式是 GoF 23 种设计模式中最为常用的行为型模式之一,它定义了对象之间一对多的依赖关系:当一个对象(主题 Subject)的状态发生改变时,所有依赖它的对象(观察者 Observer)都会自动收到通知并随之更新。本篇文章以 CS-Notes 仓库中 设计模式 - 观察者.md 为核心骨架,配合天气数据布告板(Weather Station)的完整 Java 实现逐行剖析接口定义、注册/移除/通知机制,并结合仓库中消息队列.md对观察者模式与发布-订阅模式的辨析、JDK 内置支持等内容展开纵深讲解。读完本文,你将能够独立理解观察者模式的结构语义、徒手实现一个可扩展的一对多通知系统,并能准确判断在哪些 JDK 与框架机制背后隐藏着该模式的影子。

观察者模式一对多关系示意图:持有状态的主题对象(Subject Object)自动更新/通知多个依赖对象(Dog、Duck、Cat、Mouse)

一、核心意图(Intent):一对多依赖与自动通知

观察者模式要解决的经典问题是"一处状态变化,多处联动刷新"。在 CS-Notes 的原文中,它的意图被精炼地概括为:

定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。

模式中涉及两个核心角色:

  • 主题(Subject):被观察的对象,也就是"持有状态"的一方。图中左侧的 Subject Object 正是"Object that holds state";
  • 观察者(Observer):依赖者,当主题状态变化时需要同步更新的对象。图中右侧区域被标注为 "Dependent Objects"。

上图直观展示了这种关系:一个 Subject Object 面向多个观察者对象(Dog、Duck、Cat、Mouse),通过自动的 "update/notification" 完成状态同步。这里的关键在于"自动"——主题不需要关心观察者内部如何处理状态,只需在自身状态变更后发出通知即可。

从职责上看,主题对被观察的状态与观察者的注册表负责,观察者对自己的更新逻辑负责,两者通过接口解耦,从而使依赖关系可以动态增减而不影响已有代码。

二、模式结构与角色职责

在标准实现中,主题(Subject)被设计为接口,约定三类能力:

  1. 注册观察者:新增一个订阅方;
  2. 移除观察者:取消一个订阅方;
  3. 通知观察者:状态变更时遍历观察者列表并逐一触发其更新逻辑。

观察者(Observer)同样被抽象为接口,声明一个 update() 更新入口。主题正是通过"维护一张观察者列表"来实现注册、移除与通知这三种操作的。

观察者模式 UML 类图:Subject 接口含 registerObserver/removeObserver/notifyObservers 方法,Observer 接口含 update 方法,ConcreteSubject 与 ConcreteObserver 分别实现两个接口

从类图上可以看到两条清晰的依赖链:

  • Subject(抽象接口) —— 提供 registerObserver()removeObserver()notifyObservers() 三个方法签名,ConcreteSubject(具体主题)负责实现并真正持有观察者集合与业务状态;
  • Observer(抽象接口) —— 提供 update() 方法签名,ConcreteObserver(具体观察者)实现收到通知后的实际行为。

结合原文注释可以明确一点:观察者的注册行为需要由观察者自己主动调用主题的 registerObserver() 方法完成,这一"观察者自注册"的设计在后面的天气站示例中会反复出现。

三、完整 Java 实现:天气数据布告板

CS-Notes 采用了一个经典到几乎成为观察者模式代名词的场景——天气数据布告板(Weather Station):天气数据一旦发生改变,多个布告板就要同步刷新,并且布告板的数量将来还可能继续增加。

气象站布告板示例:三个显示界面 Current Conditions(当前状况)、Weather Stats(天气统计)与 Forecast(预报)分别从同一数据源展示不同形式的天气信息

正如图中所示,"当前状况""天气统计""预报"等布告板关注同一份天气数据的不同侧面。若把刷新逻辑直接写死进天气数据类,每新增一种布告板都要改动数据源代码——观察者模式正是为了把这个"将来会继续增加"的扩展点彻底打开。

3.1 主题接口 Subject

public interface Subject {
    void registerObserver(Observer o);

    void removeObserver(Observer o);

    void notifyObserver();
}

3.2 具体主题:WeatherData

WeatherData 实现了 Subject,内部维护一张 List<Observer> 观察者列表,同时持有温度、湿度、气压三个天气数据字段:

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 setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObserver();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    @Override
    public void notifyObserver() {
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }
}

结合原文可以提炼出这段实现的几个关键设计点:

  • 数据更新即通知触发setMeasurements(...) 在写入新测量值后立即调用 notifyObserver(),保证"数据一变、通知即达"的时序一致性,这也是主题在状态变更时唯一需要主动做的事;
  • 注册表驱动通知notifyObserver() 通过 for 循环遍历 observers 列表,逐个调用 o.update(temperature, humidity, pressure),把最新数据以参数形式推送给所有观察者;
  • 安全的移除removeObserver() 先通过 indexOf(o) 定位下标,只有 i >= 0(即观察者确实存在)时才执行移除,避免对不存在的元素误删;
  • 面向接口编程WeatherData 只依赖 Observer 接口而非具体布告板类,因此新增布告板时无需改动 WeatherData 的任何代码——这正是"主题与观察者解耦"带来可扩展性的直接体现。

3.3 观察者接口 Observer

public interface Observer {
    void update(float temp, float humidity, float pressure);
}

3.4 具体观察者:两种布告板

StatisticsDisplayCurrentConditionsDisplay 是两种不同诉求的布告板,它们的结构完全同构:在构造器中接收主题对象并立刻调用 registerObserver(this) 完成自注册,随后在 update() 中定义各自的刷新行为。

public class StatisticsDisplay implements Observer {

    public StatisticsDisplay(Subject weatherData) {
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temp, float humidity, float pressure) {
        System.out.println("StatisticsDisplay.update: " + temp + " " + humidity + " " + pressure);
    }
}
public class CurrentConditionsDisplay implements Observer {

    public CurrentConditionsDisplay(Subject weatherData) {
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temp, float humidity, float pressure) {
        System.out.println("CurrentConditionsDisplay.update: " + temp + " " + humidity + " " + pressure);
    }
}

值得指出的是"构造器自注册"这一惯用法:观察者的构造函数接收 Subject 类型参数并主动订阅,从而把"谁订阅谁"的绑定关系收敛到观察者一侧。之后如果某个布告板不再需要数据,只需要让代码持有其引用并调用 weatherData.removeObserver(display) 即可从通知链上摘除。

3.5 客户端组装:WeatherStation

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);

        weatherData.setMeasurements(0, 0, 0);
        weatherData.setMeasurements(1, 1, 1);
    }
}

运行 WeatherStation 后控制台输出如下(与原文一致):

CurrentConditionsDisplay.update: 0.0 0.0 0.0
StatisticsDisplay.update: 0.0 0.0 0.0
CurrentConditionsDisplay.update: 1.0 1.0 1.0
StatisticsDisplay.update: 1.0 1.0 1.0

输出序列验证了两个事实:其一,setMeasurements()每次调用都会把全部已注册观察者各通知一遍,且通知顺序与注册顺序一致(先 Current 后 Statistics);其二,观察者无须轮询数据源,数据更新的主动权完全由主题掌控,这正是观察者模式与"主动拉取"方案的本质区别。

四、观察者模式 vs 发布-订阅模式与消息队列

许多初学者会把观察者模式和消息队列的"发布/订阅"模型混为一谈。CS-Notes 仓库在消息队列.md中专门对二者做了辨析,其原文观点如下:

  • 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,生产者与消费者不知道对方的存在,它们之间通过频道进行通信。
  • 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,生产者向频道发送一个消息之后,就不需要关心消费者何时去订阅这个消息,可以立即返回。

从这份对比可以看出两个维度的差异:

维度 观察者模式 发布-订阅 / 消息队列
参与方耦合 主题与观察者互相知晓,观察者显式向主题注册 生产者与消费者彼此无感,通过中间频道(Channel/Queue)解耦
通信方式 同步回调:主题调用观察者方法并等待返回 异步:发送消息后即可返回,消费时机由消费者决定
扩展形式 一对多的直接依赖通知 消息可被点对点消费一次,也可被多个订阅者各自消费

正因如此,观察者模式通常用于同一进程内的组件间同步联动(例如 GUI 组件刷新、数据模型与视图同步),而消息队列适用于跨进程、异步、削峰的解耦场景。仓库消息队列.md同时给出了一个很形象的判断标准:像"注册后必须点击验证邮件才能完成注册"这类强同步业务,就不能简单替换为异步消息处理。理解这层区别,有助于在系统设计时选择正确的解耦手段。

五、JDK 与生态中的观察者模式应用

CS-Notes 原文在 "JDK" 一节指出了观察者模式在标准库与主流框架中的典型落点,它们是对"模式如何被工业界使用"的最好注解:

  • java.util.Observer / java.util.Observable:JDK 1.0 就内置的观察者接口与主题类,开发者可继承 Observable、实现 Observer 快速搭建通知机制。值得注意的是,该方案存在设计上的历史包袱(Observable 是类而非接口、需要继承使用等),从 Java 9 起已被标记为弃用(deprecated),新代码更推荐自行按接口方式实现,或在 UI 框架中使用监听器机制。上面天气站的实现本质上就是对它的"接口化"改良。
  • java.util.EventListener:Java 事件模型(如 Swing/AWT)中所有监听器接口的标记父接口。Swing 的 ActionListenerMouseListener 等典型用法正是观察者模式:按钮(主题)维护监听器列表,用户点击(状态变化)时通知所有已注册监听器。
  • javax.servlet.http.HttpSessionBindingListener:Servlet 规范中的会话绑定监听器。当对象被放入或移出 HttpSession(绑定/解绑事件发生)时,容器会回调该监听器对应方法,属于观察者思想在 Web 容器生命周期管理中的运用。
  • RxJava:响应式编程库。Observable(被观察者)与 Observer(观察者)是其核心抽象,其"上游发射事件、下游订阅响应"的模型可视作观察者模式在异步数据流与背压维度上的现代化演进。

除上述条目外,观察者模式在 Java 生态中几乎无处不在:Swing 的事件分发模型、JavaBeans 的 PropertyChangeListener(属性变更即通知)、各类 EventBus 事件总线等,底层逻辑都能回溯到本文讲解的"注册—移除—通知"三件套。

六、实践要点与常见陷阱

结合天气站的完整代码,可以把落地观察者模式的要点总结如下:

  1. 围绕接口而非具体类编程:主题只依赖 Observer 接口,观察者只依赖 Subject 接口。这样主题不感知布告板具体类型,新增布告板(如 ForecastDisplay)对现有代码零侵入。
  2. 注册时机收敛:像示例那样在观察者构造器中调用 registerObserver(this),可以有效避免"创建了观察者却忘记订阅"的疏漏。
  3. 同步回调的性能与异常考量:本模式默认是同步遍历调用,若某个观察者的 update() 耗时较长或抛出异常,会阻塞后续观察者的通知甚至中断整个遍历。工业实现中通常需要对观察者列表做异常隔离,并评估是否需要引入异步通知(异步化即向发布-订阅模型靠拢,可参考上节对比)。
  4. 注意观察者的生命周期:被移除的对象若仍被主题的列表持有引用,将造成内存泄漏,因此在观察者销毁时务必调用 removeObserver() 完成"退订"。
  5. 信息推送的粒度:天气站的实现把 temperature/humidity/pressure 三个值全部作为 update() 参数"推"给观察者;当状态对象字段较多时,也可以考虑只推送一个事件对象引用,由观察者自行按需读取(即"推/拉"两种模型的选择)。

七、在 CS-Notes 设计模式体系中的位置

在 CS-Notes 的设计模式分类中,观察者属于行为型模式,与责任链、命令、状态、策略、模板方法、访问者等并列。其完整索引见 设计模式 - 目录.md,该目录的前言指出:"设计模式是解决问题的方案,学习现有的设计模式可以做到经验复用。拥有设计模式词汇,在沟通时就能用更少的词汇来讨论,并且不需要了解底层细节。"观察者模式正是这一定义的典型代表——一旦团队掌握了"观察者"这一词汇,"某个数据源变化后所有界面要刷新"这类需求就能用最简洁的语言准确传达。

进一步地,仓库中与之关联的主题还包括:

  • 消息队列.md:观察者模式与发布-订阅模式、异步处理的详细对比(上文第四节即由此展开);
  • 面向对象思想.md 与 Java 基础.md:理解"面向接口编程""组合优于继承"等设计原则,有助于从底层思路上把握观察者模式为何如此组织接口与依赖。

整体上看,观察者模式用最小的接口契约(注册、移除、通知 + 一个更新入口)换取了主题与观察者之间最大程度的松耦合,是"开闭原则"在行为扩展上最直观的体现之一。在需要"一处变更、多处联动"且联动方数量可能持续增长的场景中,它就是那个值得优先考虑的成熟方案。

登录后查看全文
热门项目推荐
相关项目推荐