Java 23 种设计模式:从踩坑到精通 | 观察者模式 —— 发布-订阅,你每天都在用的模式
Java 23 种设计模式从踩坑到精通 | 观察者模式 —— 发布-订阅你每天都在用的模式摘要当对象间存在一对多依赖关系一个对象的状态变化需要自动通知其他对象时将通知逻辑直接写死在状态变化的代码中会导致高度耦合、难以扩展。观察者模式通过定义一种订阅机制让“主题”对象管理它的“观察者”列表状态变化时自动广播通知从而将通知逻辑与业务逻辑解耦。本文从气象站数据推送的场景出发完整讲解观察者模式的原理、UML、推模型与拉模型两种实现、与发布-订阅模式的区别并结合 JavaEventListener、Spring 事件、GuavaEventBus、RxJava 等框架应用帮你彻底掌握“状态变化即通知”的设计精髓。️本文阅读地图3 分钟速览为什么气象站不能直接调用所有面板的刷新方法推模型 vs 拉模型通知数据的两种策略手写气象站通知系统拉模型推荐Spring 事件观察者模式的开箱即用JDKObserver/ GuavaEventBus/ RxJava 如何体现面试必问观察者 vs 发布-订阅有什么区别《Java 23 种设计模式从踩坑到精通》开篇系列介绍与目录 上一篇备忘录模式 当前观察者模式 下一篇状态模式 返回系列总目录1. 从“气象站数据更新”的广播需求说起假设你在开发一个气象站监控系统。气象站采集温度、湿度数据后需要同步更新多个显示面板实时温度看板、湿度看板、趋势图、日志记录器……如果直接在气象站类中调用每个面板的更新方法classWeatherStation{privateDisplayPaneldisplay;privateLoggerlogger;privateTrendChartchart;voidupdateData(floattemp,floathumidity){this.temptemp;this.humidityhumidity;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{privateListObserverobserversnewArrayList();privatefloattemperature;privatefloathumidity;Overridepublicvoidattach(Observerobserver){observers.add(observer);}Overridepublicvoiddetach(Observerobserver){observers.remove(observer);}OverridepublicvoidnotifyObservers(){for(Observerobserver:observers){observer.update();// 只通知不给数据}}// 状态变化时自动通知publicvoidsetData(floattemperature,floathumidity){this.temperaturetemperature;this.humidityhumidity;System.out.println(【气象站】数据更新温度temperature°C, 湿度humidity%);notifyObservers();}// 提供 getter供观察者拉取所需数据publicfloatgetTemperature(){returntemperature;}publicfloatgetHumidity(){returnhumidity;}}白话气象站每次数据更新后自动喊一声“我变了”至于观察者关心温度还是湿度自己去取。4.4 具体观察者温度看板只关心温度publicclassTemperatureDisplayimplementsObserver{privateWeatherStationstation;publicTemperatureDisplay(WeatherStationstation){this.stationstation;station.attach(this);// 构造时自动订阅}Overridepublicvoidupdate(){floattempstation.getTemperature();System.out.println(【温度看板】当前温度temp°C);}}白话温度看板只拉温度湿度看板只拉湿度各自按需取数据。4.5 具体观察者日志记录器关心全部数据publicclassDataLoggerimplementsObserver{privateWeatherStationstation;publicDataLogger(WeatherStationstation){this.stationstation;station.attach(this);}Overridepublicvoidupdate(){floattempstation.getTemperature();floathumiditystation.getHumidity();System.out.println(【日志】记录数据 → 温度:temp°C, 湿度:humidity%);}}4.6 客户端WeatherStationstationnewWeatherStation();TemperatureDisplaytempDisplaynewTemperatureDisplay(station);DataLoggerloggernewDataLogger(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{privateListPushObserverobserversnewArrayList();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 JDKjava.util.Observer/Observable已过时JDK 1.0 就提供了内置的观察者模式支持。虽然从 Java 9 开始被标记为过时但它仍是理解观察者模式的绝佳起点——Observable是主题Observer是观察者。8.2 Spring 事件机制ApplicationEventEventListenerSpring 的事件驱动模型就是观察者模式的经典实现。ApplicationContext充当主题EventListener标注的 Bean 方法充当观察者。ComponentpublicclassOrderEventListener{EventListenerpublicvoidhandleOrderCreated(OrderCreatedEventevent){// 发送通知、记录日志...}}8.3 Google GuavaEventBusGuava 的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接口不依赖具体观察者接口隔离原则ISPObserver接口只定义update()方法精简迪米特法则LoD主题只知道Observer接口不知道具体观察者的内部实现 《Java 23 种设计模式从踩坑到精通》快速导航开篇系列介绍与目录上一篇备忘录模式 —— 快照与撤销给对象装一个“后悔药”当前观察者模式 —— 发布-订阅你每天都在用的模式你在这里下一篇状态模式 —— if-else 满天飞让状态自己决定行为 即将发布创建型模式汇总单例、工厂、建造者、原型结构型模式汇总适配器、装饰器、代理……行为型模式汇总观察者、策略、模板方法…… 关注《Java 23 种设计模式从踩坑到精通》用 25 篇文章彻底吃透设计模式。福利预告全系列代码及 UML 源码将在完结时统一打包开放点击「关注」「收藏」第一时间获取。下一篇状态模式if-else 满天飞让状态自己决定行为 即将发布敬请关注 除了设计模式我也在深挖智能物流实战WMS、托盘调度、机器学习落地。欢迎点击头像看看专栏 《出版社物流WMS智能调度实战》。技术相通思路可鉴。