上一篇模板方法讲的是"一个流程怎么分步走",这一篇的**观察者模式(Observer)**换个话题:一个对象的状态变了,怎么自动通知一大群关心它的对象?它是行为型里最重要、应用最广的模式之一,你熟悉的"发布-订阅"“事件驱动”“监听器”,本质上都是它。
它的核心场景一句话就能说清:一变多知——一个对象(被观察者)发生变化时,所有依赖它、订阅了它的对象(观察者)都能自动收到通知并做出反应,而被观察者不需要知道这些观察者具体是谁、有多少个。这就像你关注了一个公众号,它发文章时会自动推送给所有关注者,而它并不需要挨个记住每个粉丝是谁——它只管"发布",谁订阅了谁就收到。
我们的订单场景里有一个再典型不过的例子:订单支付成功后的后续动作。一笔订单支付成功,往往要触发一连串互不相干的事:给用户加积分、发送短信通知、通知物流开始备货、更新销量统计……这些动作会随业务不断增减(明天可能又要加个"发优惠券"“推送 App 消息”)。如果把它们全塞进"支付成功"的方法里,那个方法会越来越臃肿、和一堆下游系统死死耦合。观察者模式,就是让"支付成功"这个事件优雅地广播出去,各个下游各自订阅、各自响应。
这篇文章按这条线索展开:先看"把后续动作硬编码在一起"的困境;再引出观察者模式如何用"订阅-通知"解耦;然后讲清它的角色、以及"推模型 vs 拉模型"这个关键设计选择;接着看它在 Spring 事件、GUI 监听器里的身影;最后给出适用边界与注意事项。贯穿例是订单支付成功的通知。
目录
- 把后续动作硬编码在一起的困境
- 观察者模式:订阅与通知
- 角色,与推模型 vs 拉模型
- 现实身影:Spring 事件与监听器
- 什么时候用,以及几个注意事项
一、把后续动作硬编码在一起的困境
看订单支付成功后要做的事。最直觉的写法,是在"支付成功"的方法里,把所有后续动作一件件调过去:
publicclassOrderService{publicvoidpaySuccess(Orderorder){// 支付成功后,挨个通知下游pointService.addPoints(order);// 加积分smsService.send(order);// 发短信logisticsService.prepare(order);// 通知物流备货statService.updateSales(order);// 更新销量统计// 明天要加"发优惠券"?继续往这加...}}这段代码能跑,但毛病和前面几篇的"一坨"如出一辙:
- 紧耦合:
OrderService被迫认识积分、短信、物流、统计所有下游系统。它本该只关心"订单支付"这件事,现在却要知道一大堆和它无关的下游。 - 违反开闭原则:每加一个后续动作(发优惠券、推送 App),就得回来撬开
paySuccess方法加一行。这个方法会越来越长,变成一个牵一发动全身的雷区。 - 职责混乱:"处理支付"和"支付后做什么"两件事被搅在一起。而且这些下游动作大多是可以异步、可以失败重试的,硬编码在一起后,一个下游卡住可能拖垮整个支付流程。
问题的根源是:“事件的产生”(支付成功了)和"事件的响应"(该做哪些后续),被硬编码耦合在了一起。产生事件的一方,本不该关心"谁对这个事件感兴趣、要做什么响应"。我们真正想要的是:支付成功时,OrderService只管喊一嗓子"支付成功了!",至于谁听到了、听到后干什么,由那些关心的对象自己去订阅、自己去处理。这就是观察者模式。
二、观察者模式:订阅与通知
观察者模式的做法:定义一个"观察者"接口(所有想响应事件的对象都实现它);被观察者内部维护一个观察者列表,提供"订阅/取消订阅"的方法;当自身状态变化时,遍历列表、逐个通知。
第一步,定义观察者接口:
// 观察者:所有想响应"支付成功"的下游都实现它publicinterfaceOrderObserver{voidonPaySuccess(Orderorder);}第二步,每个下游动作是一个具体观察者:
publicclassPointObserverimplementsOrderObserver{publicvoidonPaySuccess(Orderorder){/* 加积分 */}}publicclassSmsObserverimplementsOrderObserver{publicvoidonPaySuccess(Orderorder){/* 发短信 */}}publicclassLogisticsObserverimplementsOrderObserver{publicvoidonPaySuccess(Orderorder){/* 通知物流 */}}第三步,被观察者(主题)维护观察者列表,状态变化时广播:
publicclassOrderSubject{// 观察者列表privatefinalList<OrderObserver>observers=newArrayList<>();publicvoidsubscribe(OrderObservero){observers.add(o);}// 订阅publicvoidunsubscribe(OrderObservero){observers.remove(o);}// 取消订阅// 状态变化:支付成功,广播给所有观察者publicvoidpaySuccess(Orderorder){// ... 处理支付本身for(OrderObservero:observers){o.onPaySuccess(order);// 逐个通知,不关心它们具体是谁}}}用起来,下游各自订阅,OrderSubject对它们一无所知:
OrderSubjectsubject=newOrderSubject();subject.subscribe(newPointObserver());// 积分订阅subject.subscribe(newSmsObserver());// 短信订阅subject.subscribe(newLogisticsObserver());// 物流订阅subject.paySuccess(order);// 一喊,三个观察者全部自动响应对比第一节,升级点非常清晰:OrderSubject再也不认识积分、短信、物流这些具体下游了,它只认识OrderObserver接口;要加"发优惠券",新建一个CouponObserver订阅进去就行,OrderSubject一个字都不用改——完美符合开闭原则。事件的产生方和响应方彻底解耦了。用一张图看这个"一变多知"的广播结构最清楚:
图里最该记住的,是那条从主题到观察者的单向广播箭头,以及主题只依赖OrderObserver接口这个抽象。被观察者持有的是一个"观察者接口的列表",而不是任何具体观察者——这正是解耦的关键,和前面组合、策略里"持有抽象"的思路一脉相承。谁想响应,谁就实现接口、订阅进来;主题只管广播,不管谁在听。
三、角色,与推模型 vs 拉模型
观察者模式的角色,四个:
| 角色 | 本例中是谁 | 职责 |
|---|---|---|
| 抽象主题(Subject) | OrderSubject(可抽象成接口) | 维护观察者列表,提供订阅/通知方法 |
| 具体主题 | 同上的实现 | 状态变化时通知所有观察者 |
| 抽象观察者(Observer) | OrderObserver接口 | 定义响应事件的方法 |
| 具体观察者 | PointObserver等 | 实现具体的响应逻辑 |
结构讲完,有一个重要的设计选择必须讲清楚——通知时,主题该给观察者传多少数据?这就是"推模型"和"拉模型"的区别:
- 推模型(Push):主题在通知时,主动把数据"推"给观察者。就像上面代码里
onPaySuccess(Order order),主题直接把整个order塞给观察者。观察者拿到就能用,简单直接。缺点是:主题得预设"观察者需要什么数据",如果不同观察者需要的数据不同,要么传一大坨(浪费),要么众口难调。 - 拉模型(Pull):主题通知时只告诉观察者"我变了",不传具体数据;观察者收到通知后,自己主动去"拉"它需要的那部分数据。比如
onEvent(OrderSubject subject),把主题自己传过去,观察者按需调subject.getXxx()取数据。灵活,各观察者取各自需要的,但观察者要多做一步"拉取",且和主题的耦合稍紧一点。
// 推模型:主题主动推数据voidonPaySuccess(Orderorder);// 拿到就是全部数据// 拉模型:主题只通知,观察者自己拉voidonEvent(OrderSubjectsubject);// 需要什么,自己 subject.getXxx()怎么选?推模型适合"所有观察者需要的数据基本一致、且不多"的场景,简单高效;拉模型适合"不同观察者需要的数据差异大、或数据量大"的场景,更灵活。实际项目里,推模型用得更多(直接传一个封装好的"事件对象"),因为它省事;但要注意别把主题的"通知数据"设计得太胖。两者没有绝对优劣,看数据的分布来定。
四、现实身影:Spring 事件与监听器
观察者是应用最广的模式之一,尤其在"事件驱动"的场景里:
- Spring 的事件机制(ApplicationEvent + ApplicationListener):这是观察者在框架里的工业级实现。你发布一个事件
applicationContext.publishEvent(new OrderPaidEvent(order)),所有@EventListener标注的方法(观察者)会自动收到并处理。发布者和监听者完全解耦——这正是把订单支付通知"观察者化"的标准做法:支付成功后publishEvent,积分、短信、物流各写一个@EventListener监听,新增响应只加监听器、不动发布方。而且 Spring 事件还能配@Async做异步,天然解决了第一节说的"下游卡住拖垮主流程"的问题。 - GUI 事件监听器:所有图形界面的"按钮点击监听"“鼠标事件监听”(如 Swing 的
addActionListener、前端的addEventListener)都是观察者——你给按钮注册一个监听器(订阅),按钮被点击(状态变化)时通知所有监听器。 - 消息队列的发布-订阅:Kafka、RabbitMQ 的 pub-sub 模型,是观察者思想在分布式层面的放大——生产者发消息到 topic,所有订阅该 topic 的消费者都收到。
- JDK 的
java.util.Observer/Observable:JDK 自带的观察者实现(不过已在 Java 9 被标记废弃,因为设计较简陋,现在更推荐用 Spring 事件或自己实现)。
一个识别信号:凡是"一个动作发生后,要触发一串互不相干的、且会增减的后续响应",观察者(事件驱动)就是标准解法。
五、什么时候用,以及几个注意事项
适合用观察者的信号:
- 一个对象的状态变化,需要自动通知一批其他对象,且这批对象会动态增减;
- 你想让"事件的产生方"和"响应方"解耦,产生方不该知道有哪些响应方;
- 存在天然的"一对多依赖"关系(一变,多个跟着变)。
不必用的信号:
- 就一个固定的后续动作,永远不会增减——那直接调用更清晰,套观察者是过度设计;
- 响应方之间有严格的先后依赖、或需要拿到彼此的结果——观察者的各观察者是平行独立的,这种有依赖的编排更适合别的方式(比如责任链、或直接编排)。
几个容易踩的坑,务必注意:
- 通知顺序不保证:观察者被通知的顺序,通常就是它们订阅的顺序,但你不该依赖这个顺序。如果多个响应之间有严格先后要求,观察者模式不合适——那是下一篇"责任链"或直接编排的活。
- 异常传播:如果用同步通知(像第二节的
for循环),某个观察者抛了异常,会中断后面观察者的通知,甚至影响主题本身。生产环境要么捕获每个观察者的异常(一个失败不影响其他),要么用异步。 - 内存泄漏:观察者订阅后如果忘了取消订阅,主题会一直持有它的引用,导致它无法被回收——这是观察者模式一个经典的内存泄漏点(尤其在长生命周期的主题 + 短生命周期的观察者场景)。记得在合适的时机
unsubscribe。
判断的核心还是那句话:先确认真的存在"一对多、且会动态增减的通知关系",观察者才值得上;而且要想清楚同步还是异步、异常怎么处理。
小结。观察者模式解决"一变多知"的问题:被观察者维护一个观察者列表,状态变化时逐个通知,而它只依赖观察者接口、不认识具体观察者——从而把"事件的产生"和"事件的响应"彻底解耦,新增响应只需订阅、不改动发布方。它有推模型(主题推数据,简单)和拉模型(观察者拉数据,灵活)两种数据传递方式。Spring 的事件机制、GUI 监听器、消息队列的 pub-sub,都是它的身影,是事件驱动架构的基石。用它时要注意通知顺序、异常传播和内存泄漏三个坑。下一篇我们讲责任链模式——如果说观察者是"一个事件广播给所有人、各自独立处理",那责任链是"一个请求沿着一条链传递,由链上的节点依次处理、或拦截",典型如下单前的一串风控校验。