21|观察者:发布事实,让订阅者自行响应
配套代码: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 设计模式学习指南