Java 23 种设计模式:从踩坑到精通 | 观察者模式 —— 发布-订阅,你每天都在用的模式
摘要:当对象间存在一对多依赖关系,一个对象的状态变化需要自动通知其他对象时,将通知逻辑直接写死在状态变化的代码中会导致高度耦合、难以扩展。观察者模式通过定义一种订阅机制,让“主题”对象管理它的“观察者”列表,状态变化时自动广播通知,从而将通知逻辑与业务逻辑解耦。本文从气象站数据推送的场景出发,完整讲解观察者模式的原理、UML、推模型与拉模型两种实现、与发布-订阅模式的区别,并结合 Java
EventListener、Spring 事件、GuavaEventBus、RxJava 等框架应用,帮你彻底掌握“状态变化即通知”的设计精髓。
🗺️本文阅读地图(3 分钟速览)
- 为什么气象站不能直接调用所有面板的刷新方法?
- 推模型 vs 拉模型:通知数据的两种策略
- 手写气象站通知系统(拉模型,推荐)
- Spring 事件:观察者模式的开箱即用
- JDK
Observer/ GuavaEventBus/ RxJava 如何体现- 面试必问:观察者 vs 发布-订阅,有什么区别?
📖《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 上一篇:备忘录模式 |当前:观察者模式| 下一篇:状态模式
🔗 返回系列总目录
1. 从“气象站数据更新”的广播需求说起
假设你在开发一个气象站监控系统。气象站采集温度、湿度数据后,需要同步更新多个显示面板:实时温度看板、湿度看板、趋势图、日志记录器……如果直接在气象站类中调用每个面板的更新方法:
classWeatherStation{privateDisplayPaneldisplay;privateLoggerlogger;privateTrendChartchart;voidupdateData(floattemp,floathumidity){this.temp=temp;this.humidity=humidity;display.refresh(temp,humidity);logger.log(temp,humidity);chart.update(temp,humidity);}}这种写法的问题显而易见:
- 气象站与每个输出设备紧耦合,新增一个设备(如手机推送)就要改气象站代码;
- 设备增减、替换都需要修改气象站类,违反开闭原则;
- 单元测试时气象站必须带着所有设备一起测,无法独立测试。
观察者模式(Observer Pattern)正是为此而生:它定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
1.1 你的场景该不该用观察者?
| 判断标准 | 是 → 用观察者 | 否 → 用其他方式 |
|---|---|---|
| 一个对象的状态变化需要通知多个其他对象 | ✅ | ❌ |
| 被通知的对象列表可能动态增减 | ✅ | ❌ |
| 希望通知逻辑与业务逻辑解耦 | ✅ | ❌ |
| 只有一对一通知,或通知列表固定 | ❌ | 直接调用即可 |
2. 模式定义与 UML 结构
观察者模式定义对象间的一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。它属于行为型设计模式。
图文解析(配合上述 UML 图)
观察者模式的核心角色:
- 抽象主题(
Subject):定义添加、删除、通知观察者的接口,持有观察者集合。它知道有哪些观察者,但不关心它们具体做什么。 - 具体主题(
ConcreteSubject):实现主题接口,当内部状态变化时主动调用notifyObservers()通知所有观察者。 - 抽象观察者(
Observer):定义一个更新接口,当主题变化时被调用。 - 具体观察者(
ConcreteObserverA/ConcreteObserverB):实现更新接口,在update()中获取主题的最新状态并执行自己的逻辑。
核心机制:主题只负责“我有新数据了,你们自己来拿”,观察者各自决定要取哪些数据、做什么处理。这就是“拉模型”的核心思想。
3. 推模型 vs 拉模型
观察者模式有两种通知风格:
| 对比维度 | 推模型 | 拉模型 |
|---|---|---|
| 通知方式 | 主题将所有数据推送给观察者 | 主题只通知“我变了”,观察者主动拉取所需数据 |
| 灵活性 | 较低,观察者被动接受全部数据 | 较高,观察者按需获取 |
| 耦合度 | 主题需要知道观察者需要什么数据 | 观察者需要持有主题引用并知道有哪些数据 |
| 适用场景 | 数据量小、所有观察者需求一致 | 数据量大、观察者需求各异 |
实际开发中拉模型更常用,因为它给了观察者更大的自由度。下面分别提供两种模型的实现。
4. 代码实现:气象站(拉模型,推荐)
4.1 抽象观察者
publicinterfaceObserver{voidupdate();// 主题只通知“我变了”,不带数据}💬白话:观察者只需要知道“变了”,至于变什么、取什么数据,自己决定。
4.2 抽象主题
publicinterfaceSubject{voidattach(Observerobserver);voiddetach(Observerobserver);voidnotifyObservers();}💬白话:主题提供“关注”、“取关”和“喊一声”三个能力。
4.3 具体主题:气象站
publicclassWeatherStationimplementsSubject{privateList<Observer>observers=newArrayList<>();privatefloattemperature;privatefloathumidity;@Overridepublicvoidattach(Observerobserver){observers.add(observer);}@Overridepublicvoiddetach(Observerobserver){observers.remove(observer);}@OverridepublicvoidnotifyObservers(){for(Observerobserver:observers){observer.update();// 只通知,不给数据}}// 状态变化时自动通知publicvoidsetData(floattemperature,floathumidity){this.temperature=temperature;this.humidity=humidity;System.out.println("【气象站】数据更新:温度="+temperature+"°C, 湿度="+humidity+"%");notifyObservers();}// 提供 getter,供观察者拉取所需数据publicfloatgetTemperature(){returntemperature;}publicfloatgetHumidity(){returnhumidity;}}💬白话:气象站每次数据更新后自动喊一声“我变了”,至于观察者关心温度还是湿度,自己去取。
4.4 具体观察者:温度看板(只关心温度)
publicclassTemperatureDisplayimplementsObserver{privateWeatherStationstation;publicTemperatureDisplay(WeatherStationstation){this.station=station;station.attach(this);// 构造时自动订阅}@Overridepublicvoidupdate(){floattemp=station.getTemperature();System.out.println("【温度看板】当前温度:"+temp+"°C");}}💬白话:温度看板只拉温度,湿度看板只拉湿度,各自按需取数据。
4.5 具体观察者:日志记录器(关心全部数据)
publicclassDataLoggerimplementsObserver{privateWeatherStationstation;publicDataLogger(WeatherStationstation){this.station=station;station.attach(this);}@Overridepublicvoidupdate(){floattemp=station.getTemperature();floathumidity=station.getHumidity();System.out.println("【日志】记录数据 → 温度:"+temp+"°C, 湿度:"+humidity+"%");}}4.6 客户端
WeatherStationstation=newWeatherStation();TemperatureDisplaytempDisplay=newTemperatureDisplay(station);DataLoggerlogger=newDataLogger(station);station.setData(26.5f,65.0f);station.setData(27.0f,60.0f);输出:
【气象站】数据更新:温度=26.5°C, 湿度=65.0% 【温度看板】当前温度:26.5°C 【日志】记录数据 → 温度:26.5°C, 湿度:65.0% 【气象站】数据更新:温度=27.0°C, 湿度=60.0% 【温度看板】当前温度:27.0°C 【日志】记录数据 → 温度:27.0°C, 湿度:60.0%新增一个湿度看板,只需新建一个HumidityDisplay类并在构造函数中attach,气象站代码零修改。
5. 代码实现:推模型(完整示例)
推模型在update()时直接把数据推送给观察者。
// 抽象观察者(带参数)publicinterfacePushObserver{voidupdate(floattemperature,floathumidity);}// 具体观察者publicclassTempDisplayimplementsPushObserver{@Overridepublicvoidupdate(floattemperature,floathumidity){System.out.println("【推模型-温度看板】温度:"+temperature+"°C");}}// 主题publicclassPushWeatherStation{privateList<PushObserver>observers=newArrayList<>();publicvoidattach(PushObservero){observers.add(o);}publicvoiddetach(PushObservero){observers.remove(o);}publicvoidnotifyObservers(floattemp,floathumidity){for(PushObservero:observers){o.update(temp,humidity);// 推送全部数据}}publicvoidsetData(floattemp,floathumidity){notifyObservers(temp,humidity);}}💬白话:推模型简单直接,但所有观察者都要接收全部数据。如果新增一个只需要湿度的观察者,它也得接收温度数据。
6. 优缺点一览
| 优点 | 缺点 |
|---|---|
| 解耦:主题与观察者松耦合,各自可独立变化 | 通知顺序不可控:观察者的通知顺序取决于集合的实现 |
| 广播通知:一对多通知自动完成 | 性能开销:观察者多时,每次状态变化都要遍历通知 |
| 开闭原则:新增观察者无需修改主题代码 | 可能导致循环依赖:观察者又修改了主题状态,触发新一轮通知 |
| 动态订阅:运行时自由增加/移除观察者 | 难以调试:通知链较长时,排查问题需要追踪多个观察者的调用 |
7. 观察者模式 vs 发布-订阅模式 vs 中介者模式
| 对比维度 | 观察者模式 | 发布-订阅模式 | 中介者模式 |
|---|---|---|---|
| 耦合度 | 主题知道观察者列表(松耦合) | 发布者和订阅者完全解耦,通过 Broker 中转 | 同事对象通过中介者协调 |
| 通信方式 | 直接调用update() | 通过消息队列/事件总线 | 通过notify()中转 |
| 适用层级 | 进程内(单应用) | 跨进程、分布式 | 进程内,多个对象交互 |
| 典型应用 | Spring 事件、Swing 监听器 | RabbitMQ、Kafka、Redis Pub/Sub | 聊天室、DispatcherServlet |
💡简单记忆:观察者是“直接订阅,主题知情”;发布-订阅是“隔了一层 Broker,发布者不知道谁订阅”;中介者是“中间人协调双向交互”。
8. 框架与实践中的应用
8.1 JDK:java.util.Observer/Observable(已过时)
JDK 1.0 就提供了内置的观察者模式支持。虽然从 Java 9 开始被标记为过时,但它仍是理解观察者模式的绝佳起点——Observable是主题,Observer是观察者。
8.2 Spring 事件机制:ApplicationEvent+@EventListener
Spring 的事件驱动模型就是观察者模式的经典实现。ApplicationContext充当主题,@EventListener标注的 Bean 方法充当观察者。
@ComponentpublicclassOrderEventListener{@EventListenerpublicvoidhandleOrderCreated(OrderCreatedEventevent){// 发送通知、记录日志...}}8.3 Google GuavaEventBus
Guava 的EventBus提供了进程内的事件总线,支持同步和异步订阅,使用@Subscribe注解即可。
8.4 RxJava / Project Reactor
响应式编程框架将观察者模式发挥到极致,Observable(被观察者)和Observer(观察者)的命名本身就致敬了经典模式,并加入了背压、操作符等现代特性。
9. 面试必问 + 面试官追问连环炮
基础必问
- 观察者模式的核心角色?→
Subject(主题)、Observer(观察者)、ConcreteSubject、ConcreteObserver。 - 推模型和拉模型的区别?→ 推模型主题推送全部数据,拉模型只通知变化,观察者主动拉取所需数据。
- Spring 的事件机制用了什么模式?→ 观察者模式,
ApplicationContext是主题,@EventListener是观察者。
面试官追问
- “观察者模式和发布-订阅模式有什么区别?”
👉 观察者模式中主题直接维护观察者列表并调用其方法;发布-订阅模式中两者通过 Broker 解耦,通常用于分布式系统。 - “如何避免观察者之间的循环依赖?”
👉 在update()方法中避免直接修改主题状态。如果必须修改,使用标志位或异步通知打破循环。 - “Java 9 为什么废弃
Observer/Observable?”
👉 设计简陋,不支持泛型,Observable是类而非接口,灵活性差。建议使用PropertyChangeListener或 Spring 事件替代。
🎉恭喜:如果你能立刻说出 Spring 事件是观察者模式,并清楚推模型与拉模型的区别,你已经掌握了行为型模式中应用最广泛的“广播通知”设计。
10. 六大设计原则在观察者模式中的体现
| 设计原则 | 在观察者模式中的体现 |
|---|---|
| 单一职责原则(SRP) | 主题负责状态管理,观察者负责响应变化 |
| 开闭原则(OCP) | 新增观察者无需修改主题代码 |
| 里氏替换原则(LSP) | 任何Observer子类都可替换抽象观察者 |
| 依赖倒置原则(DIP) | 主题依赖抽象Observer接口,不依赖具体观察者 |
| 接口隔离原则(ISP) | Observer接口只定义update(),方法精简 |
| 迪米特法则(LoD) | 主题只知道Observer接口,不知道具体观察者的内部实现 |
🧭 《Java 23 种设计模式:从踩坑到精通》快速导航
- 开篇:系列介绍与目录
- 上一篇:备忘录模式 —— 快照与撤销,给对象装一个“后悔药”
- 当前:观察者模式 —— 发布-订阅,你每天都在用的模式(你在这里)
- 下一篇:状态模式 —— if-else 满天飞?让状态自己决定行为 🚧 即将发布
- 创建型模式汇总:单例、工厂、建造者、原型
- 结构型模式汇总:适配器、装饰器、代理……
- 行为型模式汇总:观察者、策略、模板方法……
🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
🚀下一篇:状态模式:if-else 满天飞?让状态自己决定行为!🚧 即将发布,敬请关注!
📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。