配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.observer

系列导航:Java 25 设计模式学习指南

上一篇:备忘录 · 下一篇:状态

文章发布后,收件箱、统计模块和其他功能都可能感兴趣。如果发布逻辑直接调用每个具体模块,新增一个订阅用途就要修改发布者。

观察者把兴趣登记在主题上。主题只向 PublicationListener 发出“文章已发布”的事实,具体订阅者决定自己的反应。这里使用显式接口和类,保持 OOP 角色清晰。

从问题提炼设计意图

观察者建立一对多依赖:主题发生事件时通知已登记的观察者,使观察者可以独立增加、移除或替换。

对象职责与协作关系

示例角色 职责
PublicationHub 主题,保存订阅关系并派发事件
PublicationListener 观察者接口
InboxListener 具体观察者,记录已收到标题
Demo 装配订阅关系并发布事件
classDiagram
    PublicationListener <|.. InboxListener
    PublicationHub o-- PublicationListener : subscribers
    Demo ..> PublicationHub : publishes

代码思路:变化应该落在哪个对象上

subscribe 维护去重列表,unsubscribe 移除订阅。这里根据 contains 的相等语义去重;示例观察者没有覆写 equals,因此表现为同一对象不重复登记。如果未来自定义相等性,需要重新确认订阅身份规则。

publish 先复制订阅列表,再按登记顺序逐个同步调用。回调里取消某个观察者,只影响下一次事件;当前事件仍按照开始时的快照派发。复制列表是为了解决回调中的订阅变化,不代表解决了跨线程并发修改。

异常策略同样明确:一个观察者抛出异常,异常传播给发布者,后续观察者不再接收本次事件。测试用故意失败的观察者证明这一点。若业务需要逐个隔离失败,应改用汇总结果或独立投递策略,不能靠调用者猜测。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.observer,源码目录为 src/main/java/com/hanserwei/patterns/observer/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

PublicationHub.java

package com.hanserwei.patterns.observer;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/** 管理订阅并同步分发发布事件的主题对象. */
public final class PublicationHub {
  /** 去重且按登记顺序排列的观察者列表. */
  private final List<PublicationListener> listeners = new ArrayList<>();

  /** 登记非空观察者;同一对象重复登记不重复通知. */
  public void subscribe(PublicationListener listener) {
    Objects.requireNonNull(listener, "listener");
    if (!listeners.contains(listener)) {
      listeners.add(listener);
    }
  }

  /** 取消订阅,对未登记观察者不做操作. */
  public void unsubscribe(PublicationListener listener) {
    listeners.remove(listener);
  }

  /** 按订阅快照同步通知;订阅变化从下一次事件生效,异常立即中断. */
  public void publish(String title) {
    Objects.requireNonNull(title, "title");
    for (PublicationListener listener : List.copyOf(listeners)) {
      listener.onPublished(title);
    }
  }
}

PublicationListener.java

package com.hanserwei.patterns.observer;

/** 接收文章发布事实的观察者契约. */
public interface PublicationListener {
  /** 同步处理已发布标题,异常直接向发布者传播. */
  void onPublished(String title);
}

InboxListener.java

package com.hanserwei.patterns.observer;

import java.util.ArrayList;
import java.util.List;

/** 将发布事件保存在内存收件箱的观察者. */
public final class InboxListener implements PublicationListener {
  /** 已收到的标题,保留通知顺序. */
  private final List<String> titles = new ArrayList<>();

  /** 记录本次发布的标题. */
  @Override
  public void onPublished(String title) {
    titles.add(title);
  }

  /** 返回通知结果的不可变快照. */
  public List<String> titles() {
    return List.copyOf(titles);
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.observer;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    PublicationHub hub = new PublicationHub();
    InboxListener inbox = new InboxListener();
    hub.subscribe(inbox);
    hub.publish("Java 25");
    System.out.println(inbox.titles());
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runObserver
./gradlew test --tests 'com.hanserwei.patterns.observer.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

[Java 25]

用测试确认模式的行为

重复订阅只通知一次;取消后不再收到新事件;回调修改订阅不会改变当前快照;异常立即传播并中断后续派发。

对应测试位于 src/test/java/com/hanserwei/patterns/observer/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 界面组件状态变化通知多个视图或控制器。
  • 进程内领域事件让多个功能响应同一业务事实。
  • 生命周期监听、缓存失效通知和内存事件总线。

适用边界与容易踩的坑

同步回调意味着发布者要等待全部观察者完成,慢回调会拉长发布时间;改成异步后,还需要定义执行器容量、顺序、重试和完成状态。观察者模式本身没有消息队列的持久化与可靠交付保证。

订阅方不再使用时需要取消订阅,否则主题持有的强引用会阻止其被回收。对象生命周期比 subscribe 这一行代码更重要。本例没有自动关闭订阅句柄,是为了直观展示登记与移除。

本系列不使用已经废弃的 java.util.Observable / Observer。它们与我们学习的设计思想不是同一件事;使用自定义接口不需要继承历史 API。参见 JDK 25 Observable 文档

与相近模式比较

中介者决定同事之间如何协作和路由;观察者主要广播已经发生的事实。发布订阅通常进一步解耦发送者与订阅者,可能通过中间代理和消息系统实现,但不能据此推断本例已经异步或可靠。

动手练习

新增一个失败隔离版本,要求每个观察者都有机会执行,最终返回所有失败信息。先规定顺序与错误模型,再写一个成功、失败、成功的三观察者测试,不要无记录地吞掉异常。

系列导航:Java 25 设计模式学习指南

上一篇:备忘录 · 下一篇:状态