ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

观察者模式详解:从订单报警场景到Java/C++落地与避坑

2026/9/30 8:43:54 拓冰建站 浏览量
观察者模式详解:从订单报警场景到Java/C++落地与避坑 1. 观察者模式到底解决什么痛点1.1 一个最典型的报警场景聊到观察者模式我估计很多人的第一反应是“发布-订阅”、“事件监听”——没错这确实是日常开发里出现频率最高的行为型设计模式之一。我自己十几年的体会是观察者模式是那种“看起来简单但要真正用得明白、说得清楚”的模式笔试面试要考软考要考期末要考连做MVC项目、事件驱动系统都绕不开它。先说一个最典型的场景。假设你在做一个订单系统后台管理员改了一个订单的状态比如从“待发货”改成“已发货”。这时候会发生什么系统要发短信通知买家要更新统计报表要把数据同步给仓库系统可能还要推送一条站内信。如果把这些逻辑全部塞进“订单状态修改”这个操作里代码会长成这样order.setStatus(SHIPPED); // 订单状态改完了接下来挨个通知 smsService.send(您的订单已发货); reportService.update(order); warehouseService.sync(order); messageService.push(您的订单已发货);问题来了每增加一个新的“关心订单状态”的模块就得改动订单状态修改这段核心代码。今天加个物流查询明天加个积分系统后天加个数据分析……这段代码会越来越臃肿而且下单和通知的逻辑被死死绑在一起。想单独测试订单状态更新做不到一跑就会把短信发出去。这就是耦合带来的麻烦。观察者模式解决的核心问题就是把“被观察对象”和“关心它的对象”解耦让一个对象状态变化时所有依赖它的对象都能得到通知但双方又不需要彼此认识得那么具体。1.2 耦合的本质主题认识太多不该认识的东西上面订单系统的例子本质问题出在“订单状态”这个主体Subject直接持有了短信服务、报表服务、仓库服务、消息服务的具体引用。它本来的职责只是“维护自己的状态”结果被迫知道了一堆业务模块的存在。我经常用一个生活化的类比来讲解这就像你订阅了一个健身公众号公众号每次发文章时它不需要知道每个读者是谁、在哪里、用什么设备阅读。它只维护一份“订阅者列表”文章一发布就群发推送给列表里的每一个人。新读者关注就加入列表取关就移除列表公众号本身完全不用修改。在这个类比里公众号是“被观察者”读者是“观察者”读者列表就是观察者的注册表。“发布文章”就是那个触发通知的动作。从设计角度来说这种耦合违反了依赖倒置原则。主题应该依赖“观察者接口”这个抽象而不是依赖具体的短信服务、报表服务。观察者模式的价值就是把“一对多”的依赖关系从“硬编码”变成“运行时注册”。1.3 观察者模式的解耦思路观察者模式的解耦思路其实就三步定义一个抽象观察者接口里面有一个统一的“通知入口”比如叫update。主题持有一个观察者列表提供addObserver/removeObserver方法用来注册和取消。主题状态发生变化时遍历列表调用每个观察者的update方法。这样一来主题不再认识“短信服务”“报表服务”它只认识“Observer”这个接口。新加一个订阅模块时只需要让这个模块实现 Observer 接口并注册进去主题的代码一行都不用改。开闭原则也满足了。这套思路说起来并不复杂但落地时有很多细节值得深挖后面会逐一展开。先记住一个关键点观察者模式的核心是解耦状态变更逻辑与订阅处理逻辑不是单纯地“回调”。2. 从UML类图到角色拆解一次讲透2.1 四个核心角色与职责观察者模式在23种设计模式里属于行为型模式UML类图涉及的参与者一共有四个笔试里经常让你把这些角色画出来、说清楚职责。角色名称核心职责Subject抽象主题 / 被观察者维护观察者列表提供注册、移除方法状态变化时调用notifyObserversConcreteSubject具体主题持有真正的业务状态状态改变后触发通知Observer抽象观察者定义统一的更新接口update序列图里叫“通知入口”ConcreteObserver具体观察者实现update收到通知后执行自己的业务逻辑把这些角色对照到上文订单系统的例子里订单类就是 ConcreteSubject订单状态是业务状态各种各样的服务模块短信、报表、仓库、站内信都是 ConcreteObserverSubject 和 Observer 的抽象接口就是解耦的边界。类图的结构很简单主题这边是一个关联关系持有观察者集合观察者这边是一个继承关系实现接口。这个图不用画得多花哨只要能把“一对多、依赖接口、双向解除”表达清楚就到位。2.2 推模型和拉模型怎么选观察者模式还有一个经常被问到的考点通知的时候主题到底把多少数据传给观察者这里分成了推模型和拉模型。推模型是主题update时把全部相关数据作为参数传过去。比如订单状态更新后直接把整个 Order 对象传过去或者传一个 status orderId。优点是观察者拿到数据就能干活缺点是观察者可能被强行塞了很多不需要的数据而且一旦数据字段变化接口签名也得跟着变。拉模型是主题只通知“我变了”不放数据或者只放极简标识观察者收到通知后自己到主题那边拉数据。Java 的java.util.Observable里notifyObservers(null)就是一种拉模型写法。优点是接口稳定主题不需要关心观察者要什么数据缺点是需要观察者主动去查稍微绕一点。实际项目里我倾向于推模型为主但只推“变化事件”本身而不是推全部业务数据。比如推送OrderStatusChangedEvent这个事件对象里面有订单号、旧状态、新状态、发生时间。观察者想用就用不想用也可以忽略。这个设计比纯推 / 纯拉都更实用本质上是一种“事件对象”风格的推模型。2.3 观察者模式与其他相关模式的边界很多人会把观察者模式和其他模式搞混尤其是在面试时被追问“它跟策略模式、中介者模式有什么区别”。我梳理一下最常见的几组边界。观察者模式 vs 策略模式策略模式是“把算法的变化封装成独立策略对象”解决的是“同一件事有不同的做法”观察者模式解决的是“一件事发生了要同时通知多个对象”。一个控制行为一个传播状态。观察者模式 vs 中介者模式中介者模式是“用一个中介对象封装多个对象之间的交互”比如聊天室观察者模式是“一个源广播多个接收者收听”。两者的关键区别是交互的方向中介者是网状变成星型观察者是一点到多点。观察者模式 vs 模板方法模式模板方法模式在父类里定义算法骨架子类重写部分步骤这是一个“单向继承”的关系观察者模式是“运行时注册、动态通知”结构上完全不同。记住这些边界考试和面试的时候就不容易答串。3. Java和C两种落地实现代码逐行走一遍3.1 Java版本从接口设计到并发注意事项Java 里实现观察者模式我建议不要依赖java.util.Observer和java.util.Observable因为 Observable 是个类会把继承位置占死而且从 Java 9 开始这个 API 已经标记为过时。自己写两个接口更干净也更灵活。先定义观察者接口public interface Observer { // 收到通知后的回调入口 void update(Event event); }再定义一个简单的事件对象承载变更内容public class OrderStatusChangedEvent { private final long orderId; private final String newStatus; private final long timestamp; public OrderStatusChangedEvent(long orderId, String newStatus) { this.orderId orderId; this.newStatus newStatus; this.timestamp System.currentTimeMillis(); } public long getOrderId() { return orderId; } public String getNewStatus() { return newStatus; } public long getTimestamp() { return timestamp; } }然后是主题也就是被观察者。这里我加了一个副本遍历的技巧后面讲坑的时候会展开说import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class OrderSubject { // 注意这里用 CopyOnWriteArrayList而不是普通 ArrayList private final ListObserver observers new CopyOnWriteArrayList(); private String status; public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void changeStatus(String newStatus) { this.status newStatus; notifyObservers(new OrderStatusChangedEvent(orderId, newStatus)); } private void notifyObservers(OrderStatusChangedEvent event) { for (Observer observer : observers) { observer.update(event); } } }最后是观察者的实现public class SmsNotifyObserver implements Observer { Override public void update(Event event) { OrderStatusChangedEvent e (OrderStatusChangedEvent) event; System.out.println(发送短信订单 e.getOrderId() 状态变更为 e.getNewStatus()); } }这样当changeStatus被调用时所有注册过的观察者都会依次收到通知。要新加一个“邮件通知观察者”实现接口、注册进去即可OrderSubject 一行都不用改。这个 Java 版本最大的特点是“接口驱动、集合安全”。用事件对象而不是直接传裸参数可以让 notify 方法的签名保持稳定后面加字段也不会破坏调用链。3.2 C版本生命周期和资源管理的坑C 里的观察者模式写法和 Java 有很大不同最大区别在于“观察者的生命周期”需要靠人维护。没有垃圾回收机制如果一个观察者对象被销毁了但主题的观察者列表里还存着它的指针那下一次通知就会触发野指针程序直接崩溃。先看一个最基本的写法#include iostream #include vector #include memory #include string class Observer { public: virtual ~Observer() default; virtual void update(const std::string message) 0; }; class Subject { public: void attach(std::shared_ptrObserver observer) { observers.push_back(std::move(observer)); } void notify(const std::string message) { for (const auto observer : observers) { observer-update(message); } } private: std::vectorstd::shared_ptrObserver observers; };这里我用std::shared_ptr来管理观察者指针确保主题持有观察者期间对象不会被提前释放。这是 C 实现观察者模式最关键的一条防御策略。但只有一个 shared_ptr 还不够因为还有个反向引用的问题观察者想主动退订但在update回调里它拿不到自己对应的那个 shared_ptr。更常见的做法是让观察者在析构函数里主动退订这需要观察者持有主题的引用class ConcreteObserver : public Observer, public std::enable_shared_from_thisConcreteObserver { public: explicit ConcreteObserver(Subject subject) : subject(subject) { // 构造时注册 } ~ConcreteObserver() override { // 析构时一定要退订否则主题还握着指向已销毁对象的弱引用 } void update(const std::string message) override { std::cout 收到消息 message std::endl; } private: Subject subject; };这段代码里有个隐藏问题如果观察者和主题互相持有shared_ptr 会造成循环引用导致内存泄漏。实际项目里我更推荐一个组合策略主题用std::vectorstd::weak_ptrObserver持有观察者通知时先尝试lock()能锁住就调用锁不住说明观察者已经死亡直接跳过并标记清理。观察者构造时传入主题引用并注册析构时主动退订。或者引入中间的生命周期令牌让退订操作不依赖对象本身的析构顺序。很多 C 项目还喜欢用std::function而不是继承观察者接口让观察者回调以函数对象的形式注册class EventBus { public: using Handler std::functionvoid(const std::string); void subscribe(Handler handler) { handlers.push_back(std::move(handler)); } void publish(const std::string message) { for (const auto handler : handlers) { handler(message); } } private: std::vectorHandler handlers; };这种写法自由度更高注册 lambda 就行但要注意代码可读性和类型安全会比接口方式差一点。面试如果被问到“C 和 Java 实现观察者模式的差异”核心答两点Java 靠 GC 兜底C 必须自己管理生命周期C 可以用 std::functionJava 通常用接口回调和匿名类/lambda。3.3 语言自带支持与第三方库方案对比除了自己从零实现不同语言其实都有对观察者模式的封装。这个知识点面试高频我列个对比表方便你理解技术方案用法特点典型场景Java 原生 Observer/Observable类而非接口继承受限Java 9 起过时老项目、教学演示Java Swing/AWT 事件监听addActionListener 之类处处可见观察者GUI 按钮点击、窗口事件C# event / delegate语言级关键字天生为观察者设计几乎所有 C# 事件系统C Boost.Signals2线程安全、支持返回值聚合复杂 C 项目中替代手写观察者Qt 信号槽 (signal/slot)编译期连接支持自动断开Qt GUI 框架、跨线程通信Spring ApplicationEvent基于观察者思想的事件发布/监听Spring 应用解耦业务我最常被问到的是 C# 的 event。它的本质就是观察者模式的语言级封装编译器帮你生成 add/remove 访问器然后内部维护一个委托链表。你只需要publisher.Event handler就是注册-就是退订全是语法糖比 Java 手写 attach/detach 更简洁。而对于 C 项目如果你的代码里已经用了 Boost那直接用 Boost.Signals2 会省很多事。它的好处是内部支持 mutex多线程环境下不需要自己加锁还可以做自动断连能少踩一半生命周期管理的坑。唯一的缺点是新引入一个库团队不一定接受。4. 观察者模式在真实项目里的应用场景4.1 MVC架构中的观察者Model与View的分工观察者模式最出名的应用就是 MVC 架构。Model数据模型是被观察者View视图是观察者Controller 负责把用户输入转成对外部状态变更的操作。Model 一变所有注册过的 View 自动刷新这就是观察者模式的典型架构。最典型的就是表格类应用用户在一个表格里修改了某一行的数据Model 更新后统计视图、图表视图、明细视图都同步刷新而这些视图互相之间根本不知道对方存在。用观察者模式之后新增一个视图只需要注册进去Controller 和 Model 完全不用改。这个例子在软考里经常被拿来出题看到“一个对象的状态改变需要同步更新多个对象”就优先想到观察者模式。4.2 事件机制与消息队列的分界线一个容易搞混的点是观察者模式和消息队列到底什么关系我的理解是观察者模式更强调“进程内、同步、对象之间的依赖解耦”消息队列则是“跨进程、异步、系统与系统的通信”。观察者模式里主题调用观察者回调是同步的要是某个观察者执行时间过长后面所有的观察者都得排队等。消息队列把消息投递到中间件生产者就认为搞定了消费者到底什么时候消费、怎么处理生产者根本不关心。实际项目里如果观察者的回调里有网络请求、数据库写入这类耗时操作我会考虑把它调成异步比如 Java 里用线程池包裹通知或者直接引入消息队列。4.3 业务实践中常见的观察者形态除了 MVC 和事件机制观察者模式在真实业务里的形态多种多样。我举几个自己实操过的例子。一个是股票行情推送。行情源作为主题每次价格变化都通知所有行情展示终端、策略引擎、风控系统。订阅关系是动态的用户点开哪个股票就注册对应代码的观察者关闭页面就退订。一个是电商订单状态机。订单状态每次流转都会触发一系列后续动作。这时候观察者模式通常和状态机模式配合使用状态机决定“状态怎么流转”观察者则负责“流转之后要通知谁”。还有一个是缓存失效通知。多级缓存架构中当数据库数据更新时通知所有缓存节点失效对应 key。这时候观察者模式配合广播机制能保证各节点数据一致性而不需要显式地告诉每个节点该做什么。这些场景的共同点都是“一对多、动态注册、业务解耦”。遇见这种需求的苗头就可以考虑观察者模式了。5. 一线开发踩过的坑问题排查与避坑技巧5.1 通知顺序问题观察者模式里最容易被忽视的问题就是通知顺序。多个观察者有依赖关系时顺序错了会导致状态不一致。举个例子订单状态变化先通知“库存服务”扣库存再通知“统计服务”加销售数——如果顺序反了统计服务可能统计到一个尚未成功扣库存的订单。解决办法有两种一是控制注册顺序attach 的顺序就是通知顺序用有序集合存储二是让观察者之间不产生依赖关系各自只依赖主题状态如果做不到就说明你的观察者设计得有问题。我个人的经验是观察者之间不允许互相依赖它们只能依赖主题的状态。如果在观察者回调里去查询另一个观察者的执行结果那就是在设计层面埋雷。5.2 内存泄漏与失效订阅Java 里面有 GC仔细观察者模式最容易出问题的是内存泄漏主题长期存活观察者短期使用但观察者忘了退订导致主题一直持有观察者引用观察者对象无法被回收。最常见的例子是 Activity 里注册了监听器但因为忘了解绑导致 Activity 泄漏。出身在客户端领域的同学应该都有血泪经验。解决办法很简单注册时记录观察者对象退出时在 onDestroy / finally 里严格退订。C 领域更严重因为不光是 GC 问题野指针可以直接把程序打崩。我强烈建议 C 实现里用weak_ptr而非shared_ptr来存储观察者列表。通知时先弱引用提升提成成功就回调提不成就说明观察者已经销毁顺便把这个失效订阅标记为待清理。5.3 多线程环境下的通知安全观察者模式天生是单线程友好的但现实里主题可能被多个线程同时修改观察者回调也可能包含耗时操作。多线程通知第一个问题是重复进入一个观察者回调里又去修改主题状态可能引发递归通知。Java 里我通常会用一个notifying标志位做重入保护或者干脆用队列把通知事件收集起来处理完当前批次再派发。第二个问题是线程安全遍历观察者列表时如果另一个线程正在 add/remove可能触发ConcurrentModificationException。这也是我在 Java 版本里用CopyOnWriteArrayList的原因。它保证读操作基于不可变快照通知线程可以安全遍历即使在遍历过程中有观察者注册或退订也不会异常退出。代价是写操作会复制整个数组频繁注册/退订时有性能开销但不频繁的场景下完全够用。C 里同样的问题需要加 mutex或者用 Boost.Signals2 这类线程安全库来兜底。5.4 常见问题速查表症状根因分析处理手段通知了但观察者没执行观察者未注册或者注册后又被退订检查 attach/detach 时序打印 observer 列表确认运行时抛 ConcurrentModificationException通知遍历中另一个线程修改了列表使用快照集合或加锁推荐 CopyOnWriteArrayList内存持续上涨对象无法回收观察者未退订主题持有长期引用生命周期管理中强制退订用弱引用降低风险程序崩溃 / 野指针C 观察者早于主题销毁主题还握着指针用 weak_ptr 提升检查或确保析构时主动退订通知顺序与业务预期不符观察者之间存在隐式依赖调整注册顺序规范“观察者只依赖主题”原则回调中重复触发通知死循环观察者回调里再次修改主题状态设置重入保护标志或用事件队列异步处理这几点都是我自己踩过或者带新人时常遇到的问题。写代码时如果一开始就把注册、退订、并发策略想清楚后期能省很多排查时间。5.5 还有一个容易被忽略的死代码维护观察者模式用多了还有个常见问题叫“僵尸观察者”业务上某个观察者已经不再需要了但是注册代码忘了删。因为主题调用它时也不报错它可能只是默默打印一行日志或者做个无意义的操作你根本发现不了。这种死代码会一直躺在系统里消耗资源、增加排查难度。我建议在观察者上线一段时间后做一次审查看看每个观察者是否真的在被需要。有一种辅助手段是在观察者注册时加上“心跳”或者监控指标一段时间没有收到通知或长期未激活就告警。这在大型系统里非常有价值。6. 软考、面试和期末复习的速记心得6.1 考法识别什么时候该想到观察者模式软考里设计模式题一般会给一段需求描述问你最适合采用哪种设计模式。我总结了一套快速识别的“信号词”出现“一个对象的状态改变时需要通知多个对象”出现“对象之间要保持同步但又想降低耦合”出现“改了一个数据界面/视图自动刷新”出现“订阅–发布、监听–触发、上车/下车”出现“多对多/一对多的依赖关系需要动态管理”只要题面里这些词出现两个以上大概率就是观察者模式。面试里更偏向于问实现细节和变体“Java 的 Observable 为什么过时了”“观察者模式和发布订阅模型有什么区别”“消息通知时观察者抛异常了怎么办”“多个观察者之间如何传递状态”“怎么做到异步通知”这些问题的答案在前面几章基本都有涉及。能把 UML 画出来、代码写出来、坑讲清楚面试官一般就满意了。6.2 记忆口诀与实际答题技巧23种设计模式的记忆口诀网上有不少版本我不爱背那种超长的因为考试时根本想不起来。对观察者模式我自己的速记是四个字观察订阅。展开来说就是“被观察者维护观察者列表状态变化触发订阅回调”。如果要把主题和观察者角色的关键点都覆盖到还有一句口诀主题管注册、通知管遍历、观察者管回调、状态管触发。虽然朴素但用在软考“画出类图”这类题目里非常稳先把角色写出来再看关系往角色里填。答题时还有一个实用技巧先画类图再写代码。哪怕是笔试题画图能让阅卷者一眼看到你会不会代码写得乱一点也影响不大。类图至少要画出 Observer 接口、Subject 类的 observer 集合和 attach/detach/notify 方法、ConcreteObserver 重写的 update 方法。期末复习时我会用“从需求倒推模式”的方式来练拿到一个需求先不说是哪种设计模式而是先画出数据流和控制流再匹配模式。这比死记硬背效果好得多。观察者模式本身不复杂但深入下去牵扯到生命周期管理、并发安全、消息解耦、架构设计算是行为型模式里实用性最高的一个。实际写代码时把这些细节处理好比你背的“标准答案”更有价值。我这几年带团队遇到新人说“观察者模式太简单”我一般只回复一句你先写一个线程安全、自动清理、不发死通知的观察者框架试试。能写明白再说简单。