资讯动态

观察者模式详解:原理、代码实现与发布订阅的区别

发布时间:2026/9/7 16:00:13 来源:尧图企业网站定制
1. 观察者模式是个什么玩意儿先聊点实际的。你肯定遇到过这种场景用户下单成功之后系统要干一堆事——发短信通知、推送订单消息给运营后台、更新库存、给用户加积分、触发风控校验。很多人的第一版代码是直接在订单服务里挨个调用一口气写五六个方法。刚开始没觉得有什么等业务迭代到第三个月你会发现订单服务越来越臃肿每次加一个新需求比如“下单后还要发优惠券”就得改订单服务的主流程代码改着改着就把下单逻辑改出bug了。这正是观察者模式要解决的核心问题。它的定义并不复杂定义对象之间一对多的依赖关系当一个对象状态发生改变时所有依赖它的对象都会收到通知并自动更新。用大白话说就是“我变了我通知你你想干啥你自己干我不关心”。这个模式属于GoF二十三种设计模式中的行为型模式家族也是实际工程里应用最广泛、出镜率最高的几个模式之一。你在前端写的addEventListener、click回调你在后端用的消息队列、事件总线底层玩的思想都是观察者模式。这篇文章我把观察者模式的原理、结构、代码实现、应用场景和踩坑经验一次性说透。你会看懂它和发布-订阅模式的区别看明白JDK自带的观察者接口为什么过了这么多年还是不推荐用也会拿到一份可以直接抄作业的消息中心设计方案。2. 观察者模式的核心结构和设计思路2.1 四个角色分别是谁观察者模式的结构非常清晰一共四个角色Subject被观察者/主题持有观察者列表提供注册、移除、通知三个基本方法。它只负责把状态变化广播出去至于谁收到消息之后干什么它不关心。Observer观察者定义接收通知的接口比如update()方法。每个具体观察者实现这个接口在收到通知后执行自己的逻辑。ConcreteSubject具体被观察者维护真实的状态数据状态变化时触发通知。ConcreteObserver具体观察者实现update()方法处理具体的业务响应。这里有一个关键点很多人会漏掉观察者模式里的通知是同步的。被观察者调notify()的时候是挨个遍历观察者列表、挨个调用观察者的update()方法所有观察者处理完notify()才返回。这个特点在后面的“常见问题”部分会展开讲因为在分布式场景下很多人把它和消息队列搞混踩了大坑。2.2 为什么说它实现了开闭原则观察者模式最核心的价值是解耦了“事件产生方”和“事件处理方”。事件产生方只需要依赖抽象的观察者接口不需要知道任何具体观察者的存在。画个简单的关系图文字版ConcreteSubject ├── ListObserver observers ├── attach(Observer o) // 注册 ├── detach(Observer o) // 移除 ├── notify() // 广播 └── stateChanged() - 业务逻辑里调用notify() ConcreteObserverA implements Observer.update() ConcreteObserverB implements Observer.update()当产品经理过来说“下单后还要给用户发一张满100减20的优惠券”你只需要新建一个CouponObserver类实现update()方法然后在初始化时注册进去订单服务的核心代码一行都不用动。这就是开闭原则的体现——对扩展开放对修改关闭。你品一下这段对比第一版耦合写法public void createOrder(Order order) { saveOrder(order); sendSms(order.getPhone(), 下单成功); sendPush(order.getUserId(), 订单消息); updateStock(order.getItems()); addPoints(order.getUserId(), order.getAmount()); }观察者模式重构后的写法public void createOrder(Order order) { saveOrder(order); // 通知所有观察者订单创建成功 orderEventPublisher.publish(new OrderCreatedEvent(order)); }订单服务的核心逻辑从“关心所有下游业务”变成“只关心自己该干的事”——保存订单然后发出一个“订单创建成功”的信号剩下的交给别人。2.3 观察者模式和发布-订阅模式的区别这个点面试常问工程上也容易搞混。一句话总结观察者模式是松耦合发布-订阅模式是解耦更彻底。观察者模式里被观察者直接持有观察者的引用虽然在构造上依赖抽象接口但两者存在直接关联目标对象知道有哪些观察者在监听自己。发布-订阅模式在中间加了一层事件通道Event Bus、消息队列、Broker。发布者和订阅者互相不知晓对方的存在发布者只管把消息丢进通道订阅者只管从通道里拿消息。打个比方观察者模式是你直接打电话给朋友告诉他你结婚了发布-订阅模式是你发了一条朋友圈谁看到谁点赞随缘你甚至不知道谁在关注你。对比维度观察者模式发布-订阅模式耦合度观察者/被观察者直接关联通过事件通道解耦是否同步通常同步通常异步消息队列性能快进程内调用有序列化和网络开销失败处理观察者报错会影响主流程消息队列有重试和死信机制典型实现JDK Observable、自定义监听器Kafka、RabbitMQ、Event Bus所以在单体应用、进程内的事件处理用观察者模式就够了。跨服务、跨系统的事件通知直接上消息队列别硬套观察者模式。3. 从零手写一个观察者模式3.1 Java代码示例下单通知场景说再多理论不如跑一遍代码。我用Java手写一个完整实现场景就是开头的用户下单通知。首先定义观察者接口public interface OrderObserver { void onOrderCreated(Order order); }然后是具体观察者——短信服务负责给用户发下单成功短信public class SmsObserver implements OrderObserver { Override public void onOrderCreated(Order order) { System.out.println([短信服务] 向 order.getPhone() 发送下单成功通知订单号 order.getOrderNo()); } }积分服务负责给用户加积分public class PointsObserver implements OrderObserver { Override public void onOrderCreated(Order order) { System.out.println([积分服务] 用户 order.getUserId() 下单成功赠送积分 order.getAmount().intValue()); } }库存服务负责扣减库存public class StockObserver implements OrderObserver { Override public void onOrderCreated(Order order) { System.out.println([库存服务] 扣减商品库存订单明细数 order.getItems().size()); } }事件发布者就是被观察者public class OrderEventPublisher { private final ListOrderObserver observers new ArrayList(); public void register(OrderObserver observer) { observers.add(observer); } public void unregister(OrderObserver observer) { observers.remove(observer); } public void publish(Order order) { for (OrderObserver observer : observers) { observer.onOrderCreated(order); } } }组装起来看效果public class OrderService { private final OrderEventPublisher publisher new OrderEventPublisher(); public OrderService() { // 可以把注册逻辑放到启动配置里用Spring直接注入 publisher.register(new SmsObserver()); publisher.register(new PointsObserver()); publisher.register(new StockObserver()); } public void createOrder(Order order) { System.out.println([订单服务] 保存订单 order.getOrderNo()); publisher.publish(order); System.out.println([订单服务] 下单流程完成); } public static void main(String[] args) { OrderService service new OrderService(); Order order new Order(); order.setOrderNo(SN20240911001); order.setUserId(1001L); order.setPhone(13800138000); order.setAmount(new BigDecimal(299.00)); ListString items new ArrayList(); items.add(机械键盘); service.createOrder(order); } }执行结果[订单服务] 保存订单SN20240911001 [短信服务] 向 13800138000 发送下单成功通知订单号SN20240911001 [积分服务] 用户 1001 下单成功赠送积分299 [库存服务] 扣减商品库存订单明细数1 [订单服务] 下单流程完成注意观察执行顺序publish()方法是同步阻塞的三个观察者依次执行完主流程才继续。这个特性有利有弊下面会专门说。3.2 用JDK自带的Observer还是自己写有些Java初学者会被建议直接继承java.util.Observable、实现java.util.Observer这在JDK 8及以前还可以玩一玩但JDK 9开始Observable和Observer已经被标记为废弃deprecated官方也明确不推荐使用。原因很简单Observable是一个类而不是接口这意味着你的被观察者如果还想继承别的父类就没办法了另外它的通知方法notifyObservers()在并发环境下用synchronized锁的是Observable对象本身粒度太粗还有线程安全的处理也不够灵活。所以我的建议是别用JDK自带的自己写或者用Guava的EventBus。自己写的核心逻辑不超过三十行可控性最强也不会有历史包袱。3.3 用Guava EventBus实现更优雅的观察者模式如果项目里已经引入了Google Guava直接用EventBus会更优雅。它借助注解和反射直接把观察者模式升级成了发布-订阅模式连接口都不用定义。public class SmsListener { Subscribe public void handleOrderCreated(OrderCreatedEvent event) { System.out.println([短信服务] 发送短信给 event.getOrder().getPhone()); } } public class OrderService { private final EventBus eventBus new EventBus(order-bus); public OrderService() { eventBus.register(new SmsListener()); eventBus.register(new PointsListener()); eventBus.register(new StockListener()); } public void createOrder(Order order) { saveOrder(order); eventBus.post(new OrderCreatedEvent(order)); } }EventBus自带线程池支持异步模式AsyncEventBus同一个事件可以被多个Subscribe方法接收而且如果监听器抛出异常默认情况下只影响当前这个监听器的处理不会中断整个事件广播这个可以通过自定义SubscriberExceptionHandler调整。4. 观察者模式在前端和其他语言里的应用4.1 JavaScript里的观察者模式前端工程师每天都在用观察者模式只是很多人没意识到。DOM事件监听是最典型的实现document.getElementById(btn).addEventListener(click, () { console.log(按钮被点击了); });这里DOM元素是被观察者你注册的click回调就是观察者。浏览器在用户点击时触发notify()调用所有注册的回调函数。Vue的数据响应式原理也大量使用了观察者模式。Vue 2用Object.defineProperty劫持数据的getter/setter每个组件实例都是一个Watcher观察者数据变化时通知对应的Watcher重新渲染Vue 3改用Proxy现在原理介绍里基本叫“依赖收集”和“派发更新”但本质还是观察者模式那一套。Node.js里的EventEmitter是更完整的观察者模式封装const EventEmitter require(events); class OrderService extends EventEmitter {} const orderService new OrderService(); // 注册观察者 orderService.on(orderCreated, (order) { console.log(发送短信, order.phone); }); orderService.on(orderCreated, (order) { console.log(赠送积分, order.userId); }); // 广播事件 orderService.emit(orderCreated, { phone: 13800138000, userId: 1001 });4.2 Python和Spring Framework里的实践Python里weakref.WeakSet配合自定义事件类就能很优雅地实现观察者模式避免因观察者持有被观察者引用导致的内存泄漏问题。这一点在长生命周期的服务里尤其重要后面重点讲。Spring Framework里更是把观察者模式发扬光大了。ApplicationEvent配合EventListener注解几乎是Spring Boot事件驱动的标准写法public class OrderCreatedEvent extends ApplicationEvent { private final Order order; public OrderCreatedEvent(Object source, Order order) { super(source); this.order order; } } Service public class SmsListener { EventListener public void onOrderCreated(OrderCreatedEvent event) { // 发短信 } }发布事件时只需要注入ApplicationEventPublisher调用它的publishEvent()方法。Spring默认同步执行事件监听器但你可以加Async注解把它变成异步的。5. 观察者模式的常见误区和排查技巧5.1 同步通知导致主流程变慢很多初学观察者模式的人会忽略“同步调用”这个特点。我接到过一个真实的生产事故订单服务接了一个营销观察者营销逻辑里调了一个外部接口这个接口响应超时拖到10秒导致用户下单接口整个超时。排查思路很简单查看接口耗时分布发现OrderEventPublisher.publish()方法耗时异常再往下追是某个观察者处理逻辑阻塞。应对方案有三种按推荐顺序排异步化观察者用线程池执行观察者的update()方法主流程只负责把事件发出去就返回。消息队列化解耦如果上下游都在同一个系统可以用EventBus的异步模式如果跨系统直接把事件发到Mq。设超时和隔离每个观察者用独立的线程池给外部调用设置合理的超时时间避免一个观察者拖死整个流程。一个相对稳妥的线程池实现public class AsyncOrderEventPublisher { private final ListOrderObserver observers new CopyOnWriteArrayList(); private final ExecutorService executor new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000), new ThreadPoolExecutor.CallerRunsPolicy() ); public void publish(Order order) { for (OrderObserver observer : observers) { executor.submit(() - observer.onOrderCreated(order)); } } }需要注意CallerRunsPolicy这个拒绝策略当线程池队列满了任务不会丢弃而是交回调用线程执行保证事件不丢失代价是主线程可能被阻塞。5.2 观察者执行顺序不可控观察者模式的遍历顺序取决于列表的插入顺序但这在多线程环境下并不保证。如果业务上对执行顺序有要求比如先扣库存、再发短信有两个解决思路一是把有先后依赖的逻辑合并成一个观察者内部再去编排执行顺序。二是给观察者加order属性通知时先排序再执行。我个人推荐前者。因为观察者模式的初衷就是让各个观察者保持独立一旦引入执行顺序依赖就破坏了解耦性也增加了维护成本。5.3 内存泄漏观察者没有被及时移除写桌面端应用或者长驻内存的服务时如果观察者注册了但不移除被观察者一直持有观察者的引用垃圾回收器就没法回收这些对象。特别是在GUI程序里窗口关掉了但监听器还在事件源上挂着窗口对象就永远无法回收。解决办法是注册时记录返回的AutoCloseable或Disposable对象销毁时调用。观察者内部用弱引用WeakReference持有被观察者。使用CopyOnWriteArrayList存储观察者支持并发安全地增删。Spring里用EventListener的bean生命周期由Spring容器管理一般不存在这个问题但如果你在代码里手动addListener记得在适当的时机removeListener。5.4 事件风暴观察者触发了连锁反应有一种隐蔽的问题是“观察者嵌套”。观察者A处理事件时又发布了一个新事件这个新事件被观察者B接收观察者B又发布了事件……如果不加限制轻则导致逻辑混乱重则引发无限递归。我遇到过一个实际例子用户修改资料后发布“资料变更事件”其中一个观察者是“审计日志观察者”它把变更记录写入数据库而数据库写入成功后又发布了一个“审计日志创建事件”这个事件又被另一个观察者监听了……排查的时候顺着调用链一层层追特别费劲。预防手段还是在设计时明确边界核心业务事件和派生业务事件分开通道或者在事件对象里加一个source标识观察者处理时判断来源避免对自己产生的事件再次响应。5.5 如何排查线上“通知没生效”的问题这类问题排查有个固定套路先确认观察者是否注册成功。检查observers列表大小或者打印注册日志。再确认事件是否发布。在publish()方法入口打日志看事件对象是否为空、字段是否完整。然后确认观察者是否收到。每个观察者update()方法入口打日志带线程号和时间戳。最后确认是否为异步线程导致的上下文丢失。比如我们把观察者改成异步执行后日志工具传递的traceId丢失了导致日志串联不起来。这里给一个建议如果是异步通知一定把traceId一并传递到线程池里不然排查问题时全靠猜。6. 观察者模式的应用场景和实战案例6.1 哪些场景最值得用观察者模式观察者模式不是万能药但下面这些场景是明确适用的事件驱动架构的进程内实现系统内的业务事件广播比如订单创建、支付回调、用户注册。UI组件解耦按钮点击、鼠标移动、组件状态变化。前端框架里的事件系统都是观察者模式。数据同步与联动某个数据源变化多个视图需要联动更新比如Excel里单元格变化后多个图表联动刷新。跨模块解耦核心模块只需要发布事件不需要import任何下游模块的类。状态监控系统状态变化时监控模块、告警模块、日志模块各自响应。6.2 实战案例简易消息中心的演进过程分享一个我实际参与过的消息中心设计这个系统从第一版到第三版正好走过了“没有模式 → 观察者模式 → 观察者模式消息队列”的演进路径。第一版硬编码调用所有系统在业务代码里直接调用消息中心提供的sendSms()、sendEmail()、sendAppPush()方法。缺点很明显业务方要了解消息中心所有能力调用逻辑散落各处消息中心一改接口所有调用方跟着改。第二版观察者模式设计了一个MessageEventPublisher业务系统只需要发布MessageEvent消息中心内部注册了短信观察者、邮件观察者和App推送观察者各自处理自己的发送逻辑。业务系统不再关心消息是怎么发出去的。这个阶段对于进程内同步发送的场景已经够用了但问题也暴露了如果用户下单后同时需要发三个渠道的通知同步发送耗时太长。第三版观察者模式 消息队列把跨系统的部分比如下单系统需要通知物流系统和积分系统改为通过MQ发送事件消息中心保留在进程内用观察者模式处理多渠道发送发送逻辑改为异步。下单系统发布事件的代码几乎没变只是底层从进程内调用变成了MQ投递。这个演进给我们的启示是观察者模式更适合处理“单机内、逻辑轻、实时性要求高”的事件响应。一旦要跨系统、要削峰填谷、要保证最终一致性还是得靠消息队列。6.3 观察者模式在系统架构中的位置在整个系统架构里观察者模式通常是最贴近业务逻辑的那一层。它不解决数据存储问题不解决网络通信问题它解决的是业务对象之间如何优雅地通知和联动。它和消息队列的关系不是替代而是互补。进程内的即时响应比如缓存更新、日志记录、内存态标记用观察者模式跨服务的可靠异步通知比如用户下单后通知仓储中心出货用消息队列。7. 观察者模式的面试高频考点和加分项第一题说说观察者模式和发布-订阅模式的区别。基础回答是“观察者模式中观察者和被观察者互相知道对方发布-订阅模式通过事件通道解耦”如果能在代码层面补充一段手写对比说明观察者里attach传的是具体观察者实例、发布-订阅里subscribe只传回调函数就能把面试官喂到八成饱。第二题JDK的Observer接口有哪些设计缺陷能答出“Observable是类不是接口影响扩展”“线程安全处理不灵活”“顺序不可控”“JDK 9已废弃”这四条基本就是满分。第三题怎么避免观察者模式导致的性能问题这个问题的正确姿势是把“同步改异步”“控制观察者数量”“加缓冲区”“限制事件传播深度”“设置超时熔断”这几点都答上并且能结合实际场景说明取舍逻辑。第四题在Spring框架中如何使用观察者模式ApplicationEventEventListenerAsync如果再能讲清楚Spring事件是默认同步的、异步时需要配置线程池就足以体现你对底层的理解。额外说一下加分项如果能聊到函数式接口ConsumerT可以让观察者的注册更加轻量或者聊到可以把观察者列表用CopyOnWriteArrayList做并发优化这些细节很能体现动手能力。8. 最后分享一点实战心得我在自己的项目里用过很多次观察者模式有一个体会特别深观察者模式解决的是类与类之间的耦合问题但它自己也会引入新的复杂度。最典型的就是“代码跳来跳去”的问题一个简单的下单流程加了三四个观察者之后代码阅读难度成倍上升。你在主流程里看不见“发短信”和“扣库存”的逻辑得点进观察者实现才能找到。所以我的经验是观察者模式适合用在那些“业务边界清晰、扩展方向明确”的场景不要为了解耦而解耦。如果只有两三个观察者而且短期内不会有新的扩展直接写代码反而更清晰。第二个心得是定义事件对象是观察者模式设计的关键。事件对象的字段就是观察者和被观察者之间的“协议”定义得不好后面所有观察者都要跟着改。我的建议是事件对象只放跟事件本身相关的数据不要图省事把整个业务对象传进去否则改一个字段会引发连锁修改。第三个心得是关于测试的。观察者模式的实现容易被忽视单元测试但这里面恰恰容易出bug。给观察者写测试有一个小技巧Mock掉被观察者只验证观察者收到事件后是否正确执行而不是真的去触发整个业务流程。同时要特别注意异步化的观察者要在测试里加CountDownLatch之类的同步机制否则测试跑完了异步线程还没执行断言永远通过不了。观察者模式是行为型设计模式里最贴近日常开发的一个无论是写代码还是做架构设计把它吃透都不亏。希望这篇文章能真正帮到你。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价