设计模式预计阅读 16 分钟6331 字3150 次浏览

设计模式修炼之路 ⑯ 观察者模式——事件订阅与广播

一对多依赖、订阅退订自由、生产环境三块拼图、Spring 事件机制、观察者 vs 发布订阅

目录

行为型第三站。

上一篇策略模式解决的是"该用哪个算法",这一篇解决"该通知谁"。

先看一段熟悉的代码。订单支付成功后要发短信、加积分、扣库存,你会在 OrderService 里怎么写?

一、观察者模式是什么

GoF 的原始定义:

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. (在对象间定义一对多依赖,当一个对象状态改变时,所有依赖它的对象都会自动收到通知并更新。)

翻译成人话:一个对象有事发生,就往一份"订阅名单"里群发通知;谁关心这事,谁去名单上登记。

四个角色:

角色 干什么 本文对应
Subject(目标/被观察者) 维护订阅名单,状态变化时挨个通知 OrderSubject
Observer(观察者接口) 收到通知后干什么,统一契约 OrderObserver
ConcreteObserver(具体观察者) 短信、积分、库存、物流,各管一摊 四个 XxxObserver
(业务方法) 触发点:支付成功这件事本身 paySuccess()

最贴切的类比是公众号:你关注了某个号(attach),号主发新文章时你自动收到推送(notify);不感兴趣了取关(detach),以后推什么都不打扰你。号主从不知道订阅者是谁、有多少人,他只管发文。

和前几篇的关系:⑮ 策略模式是 Context 持有一个策略、指哪打哪;观察者是 Subject 持有一串观察者、有事全广播。一个一对一,一个一对多——正好接着讲。

二、优点与缺点

优点:

  1. 彻底解耦。OrderSubject 只认识 OrderObserver 接口,不知道短信怎么发、积分怎么加。下游换实现、换类名,上游一行不动。
  2. 对扩展开放。新下游接入 = 写个类 + attach 一行;甚至这行都不用写(后面 Spring 部分见)。
  3. 动态订阅退订。观察者名单是运行时的,随时增减——测试里短信退订后广播立刻少一家,就是这意思。
  4. 广播效率。一次事件触发 N 个动作,触发点只有一行循环。

缺点:

  1. 观察者之间顺序不可控。积分要在库存扣完之后执行?观察者模式不保证顺序(本例按注册顺序算运气好)。有顺序依赖就别用观察者,那是 ⑰ 责任链的地盘。
  2. 通知风暴。A 通知 B,B 状态变化又通知 A……链式反应没设计好就是死循环,CPU 直接拉满。
  3. 内存泄漏风险。Subject 持有 Observer 的强引用,detach 忘了调,观察者永远无法被 GC——Swing 程序内存泄漏的老病灶。
  4. 一个观察者抛异常,广播中断。本例循环里没有 try-catch,短信挂了库存就收不到通知。生产代码必须给每个观察者的调用套上异常隔离。

三、什么时候用

判断标准:一件事发生后,有多个互不相识的下游关心,且这份名单会变。

  • 业务事件广播:支付成功、注册成功、审批通过 → 短信/积分/风控/埋点
  • 对象状态联动:表格数据变了,柱状图、折线图、导出模块都要刷新(MVC 里 View 监听 Model)
  • 跨模块解耦:订单模块不想认识物流模块,但物流想感知订单

反面对照:下游固定就一个、或者下游之间有先后顺序、或者需要拿到处理结果——都不适合观察者,那是策略(⑮)、责任链(⑰)或者直接调用的场景。

四、实现方式:把"点对点调用"改成"广播订阅"

4.1 反面教材:下单后依次调三个下游

public class BadOrderService {

    public void paySuccess(String orderId) {
        System.out.println("[订单] 订单 " + orderId + " 支付成功");
        System.out.println("  [短信] 尊敬的用户,订单 " + orderId + " 已支付");
        System.out.println("  [积分] 订单 " + orderId + " 增加 100 积分");
        System.out.println("  [库存] 扣减订单 " + orderId + " 对应库存");
        // 下个月要接物流、接风控、接数据埋点……
        // 每接一个下游,这个方法就得改一次,回归测试全量重跑
    }
}

OrderService 认识了三个下游:发短信的、加积分的、扣库存的。每接一个新下游,这个方法就得改一次——和 ⑮ 的 if-else 巨无霸是同一种病:把"变化的部分"焊死在"稳定的部分"里。区别只是策略模式里变化的是算法分支,这里变化的是下游名单。

4.2 正解:Subject 拿名单,Observer 各干各的

第一步,观察者接口——"收到订单支付通知后干什么":

public interface OrderObserver {

    void onOrderPaid(String orderId);
}

第二步,被观察者——维护名单 + 业务方法里广播:

public class OrderSubject {

    private final List<OrderObserver> observers = new ArrayList<>();

    // 订阅:观察者注册进名单
    public void attach(OrderObserver observer) {
        observers.add(observer);
    }

    // 退订:从名单里移除,之后的广播不再收到
    public void detach(OrderObserver observer) {
        observers.remove(observer);
    }

    public void paySuccess(String orderId) {
        System.out.println("[订单] 订单 " + orderId + " 支付成功,开始广播");
        for (OrderObserver observer : observers) {
            observer.onOrderPaid(orderId);
        }
    }
}

第三步,三个具体观察者:

public class SmsObserver implements OrderObserver {

    @Override
    public void onOrderPaid(String orderId) {
        System.out.println("  [短信] 尊敬的用户,订单 " + orderId + " 已支付");
    }
}

public class PointsObserver implements OrderObserver {

    @Override
    public void onOrderPaid(String orderId) {
        System.out.println("  [积分] 订单 " + orderId + " 增加 100 积分");
    }
}

public class StockObserver implements OrderObserver {

    @Override
    public void onOrderPaid(String orderId) {
        System.out.println("  [库存] 扣减订单 " + orderId + " 对应库存");
    }
}

测试类跑一遍——重点看后半段,物流中途接入、短信中途退订:

public class ObserverTest {

    public static void main(String[] args) {
        System.out.println("========== 反面教材:下单后直接调三个下游 ==========");
        new BadOrderService().paySuccess("1001");

        System.out.println();
        System.out.println("========== 正解:观察者模式 ==========");
        OrderSubject subject = new OrderSubject();
        SmsObserver sms = new SmsObserver();
        subject.attach(sms);
        subject.attach(new PointsObserver());
        subject.attach(new StockObserver());
        subject.paySuccess("2001");

        System.out.println();
        System.out.println("-- 新下游「物流」接入:不改 OrderSubject,注册即生效 --");
        subject.attach(new LogisticsObserver());
        subject.paySuccess("3001");

        System.out.println();
        System.out.println("-- 短信观察者退订(detach),广播立刻少一家 --");
        subject.detach(sms);
        subject.paySuccess("4001");
    }
}

运行输出:

========== 反面教材:下单后直接调三个下游 ==========
[订单] 订单 1001 支付成功
  [短信] 尊敬的用户,订单 1001 已支付
  [积分] 订单 1001 增加 100 积分
  [库存] 扣减订单 1001 对应库存

========== 正解:观察者模式 ==========
[订单] 订单 2001 支付成功,开始广播
  [短信] 尊敬的用户,订单 2001 已支付
  [积分] 订单 2001 增加 100 积分
  [库存] 扣减订单 2001 对应库存

-- 新下游「物流」接入:不改 OrderSubject,注册即生效 --
[订单] 订单 3001 支付成功,开始广播
  [短信] 尊敬的用户,订单 3001 已支付
  [积分] 订单 3001 增加 100 积分
  [库存] 扣减订单 3001 对应库存
  [物流] 通知仓库拣货发货:订单 3001

-- 短信观察者退订(detach),广播立刻少一家 --
[订单] 订单 4001 支付成功,开始广播
  [积分] 订单 4001 增加 100 积分
  [库存] 扣减订单 4001 对应库存
  [物流] 通知仓库拣货发货:订单 4001

4.3 三处细节咂摸一下

**细节一:订单 3001 多了一行物流,OrderSubject 没改一行代码。**写 LogisticsObserver、attach,广播自然多一家。开闭原则的达成方式和 ⑮ 一模一样:新变化 = 新类,不是改旧类。

**细节二:订单 4001 少了短信,也是运行时动态生效。**观察者名单是活的,订阅退订都是运行期行为。这是"依次调用三个方法"永远做不到的。

**细节三:广播循环只有一行 observer.onOrderPaid(orderId)。**Subject 对观察者的一切认知浓缩在这行里:它是个 OrderObserver,出了事我调它。至于它是谁、要干什么、干砸了怎么办——Subject 一概不知,也不想知道。这行代码就是"依赖倒置"的最小标本:依赖抽象,不依赖具体。

4.4 生产环境要补的三块拼图

教程版到此为止,生产代码还要补三件事:

① 异常隔离——一个观察者炸了不能连累其他观察者:

for (OrderObserver observer : observers) {
    try {
        observer.onOrderPaid(orderId);
    } catch (Exception e) {
        log.error("观察者执行失败: {}", observer.getClass().getSimpleName(), e);
    }
}

② 并发安全——如果广播的同时有别的线程 attach/detach,ArrayList 会抛 ConcurrentModificationException。监听器列表的经典选择是 CopyOnWriteArrayList(读多写少场景正合适),Swing 里则用专门的 EventListenerList。

③ 生命周期管理——长生命周期 Subject 挂着短生命周期观察者,用完必须 detach,否则就是第二节说的内存泄漏。Java 里也可以用弱引用(WeakReference)兜底,观察者没别的强引用时自动从名单里"失效"。

五、在框架里找它

**JDK 的 Observable——一个反面教材式的官方实现。**java.util.Observable 从 JDK 1.0 就有(Observable 类 + Observer 接口),但 JDK 9 起被标记 @Deprecated。为什么?它是类不是接口,想观察就得继承它,把唯一的继承名额烧了(这个理由 ⑭ 里也吐槽过);setChanged/notifyObservers 的状态耦合设计也别扭。官方亲手示范了"观察者思想的正确性"和"API 设计的失败"可以并存——思想用你的,代码自己写。

**Swing/AWT 事件监听。**每个按钮都是被观察者,每个 ActionListener 都是观察者:

button.addActionListener(e -> System.out.println("被点了"));

awt 的事件体系把观察者模式铺到了桌面编程的每个角落。

**Spring 的事件机制——Java 后端最常用形态。**发布方注入 ApplicationEventPublisher,一行发布;接收方一个注解订阅,连 attach 都省了:

// 发布:订单服务里
publisher.publishEvent(new OrderPaidEvent(orderId));

// 订阅:物流模块里
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
    logisticsService.arrangeShipment(event.getOrderId());
}

默认同步执行(发布线程里跑监听器),想异步加 @Async;想保证事务提交后才通知,用 @TransactionalEventListener——同步/异步/事务边界三个开关,把教程版观察者的坑全填了。

**Guava EventBus。**把"接口约束 + 名单维护"简化成"一个注解 + 一个 register":

eventBus.register(new LogisticsObserver());  // 无需实现任何接口

@Subscribe
public void onOrderPaid(OrderPaidEvent event) { ... }

按事件类型自动分发,Spring 的 @EventListener 明显师承于此。

**消息队列——观察者的分布式形态。**RocketMQ/Kafka 的发布订阅,把"Subject 持有 Observer 引用"换成"大家都只连 Broker":生产者发消息,N 个消费组各自消费。观察者模式的解耦思想放大到跨进程,就成了 MQ。第六节接着这个话头展开。

六、观察者 vs 发布订阅——面试高频题

很多文章把两个词混着用,面试官却爱较真。区别一句话:看有没有中间人。

观察者模式 发布订阅模式
结构 Subject **直接持有** Observer 引用 发布者和订阅者**互不相识**,中间隔着事件总线/Broker
耦合 松耦合,但同在一个进程里 完全解耦,可跨进程、跨机器
时机 通常同步,通知完才返回 通常异步,发完即走
典型 本篇 OrderSubject、Swing MQ、Redis pub/sub、Spring 事件(半个)

本篇手写的 GoF 版是纯观察者;Spring 事件因为引入了 ApplicationEventPublisher 这个"隐形中间人",已经带一点发布订阅的意思;到了 MQ 就是彻底的发布订阅。观察者是设计模式,发布订阅是架构风格——这么答,面试官一般会点头。

写在最后

观察者模式一句话总结:一方有事,名单广播;订阅自愿,退订自由。

它和策略模式合起来覆盖了行为型的两大解耦方向:⑮ 解耦"用哪个算法"(一对一),⑯ 解耦"通知哪些下游"(一对多)。下一篇 ⑰ 责任链模式补上第三种:请求交给谁处理——报销单从你手里递出去,组长批不过找经理,经理批不过找总监,请求沿着链条一站一站传下去,谁有权限谁处理。审批流的经典实现,下一篇见。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论