订单付款后要通知日志与开票两个本地回调。第一个回调抛错时第二个还需收到事件;开票回调又可能在处理 paid 时发布 invoiced。如果直接遍历订阅列表,回调失败会阻断后续通知,重入时顺序还可能让人意外。同步事件的顺序与失败到底由谁规定?

订阅表不等于交付保证

Before.publish 直接遍历监听器;在第一个回调抛 IllegalStateException 的反例中,发布调用抛错,后面的回调不会执行。After 维护订阅列表与一个待处理事件队列:事件按发布顺序入队,正在派发时的重入事件留到当前事件的所有订阅者处理完再派发;每轮取订阅快照,监听器抛出的 RuntimeException 被记录到 failures(),其他监听器仍被调用。测试轨迹是 first:paid, second:paid, first:invoiced, second:invoiced。退订后再次发布不会给被退订者送达。

1
2
3
4
after.subscribe(event -> {
if (event.equals("paid")) after.publish("invoiced");
});
after.publish("paid");

这是本章 Observer 的通知关系:发布者不需要知道订阅者具体处理什么;但错误策略、顺序、重入与退订语义不能仅靠“订阅”两个字得到。Alternative 在固定两位调用者时直接顺序调用,最容易理解;若订阅方不会变化,维护通用注册表不是必需的。与 Mediator 的集中协调多方交互不同,Observer 在这里是发布一个事实、由多个本地回调响应;与责任链不同,它不因前一位成功就停止后续派发。当前 failures() 累积错误但不自动重试,发布返回并不代表异步或持久化交付成功。若回调无限重发同类事件,队列也可能持续增长。

方案 两位正常监听者 第一位失败 / 重入
Before 按注册顺序调用 抛错后停止;无重入策略
After 按注册顺序调用 记录错误继续;重入排到本轮之后
Alternative 两次直接调用 顺序明确,但错误和变化由调用者自己决定

没有测试并发注册、跨进程消息、重复消费、队列持久化或无限重入阻断。

flowchart LR
    Publisher[发布方] --> Bus[After.publish]
    Bus --> Pending[pending 队列]
    Pending --> Snapshot[listeners 快照]
    Snapshot --> Listener[Consumer.accept]
    Listener -.重入 publish 入队.-> Bus
    Listener -.失败记录.-> Failures[failures]

验证与练习

在 examples/design-patterns/ 执行 ./mvnw -B -ntp -pl labs/31 -am test、./mvnw -B -ntp verify;原始版本、输入、退出码与输出见 examples/design-patterns/evidence/31/RUN.md。正常订阅是旧合同,异常继续与重入顺序是新增断言。

  1. 新增“某订阅者处理一次后自动退订”,先写原有两个监听者的回归和退订/重入的新轨迹,再确定当前事件快照是否仍应收到一次通知。
  2. 把订阅者固定为两个普通函数,删去事件中心并运行旧合同;说明何时动态订阅才值得恢复 Observer。

参考资料

上一节:30 请求对象能撤销什么;下一节:32 审核何时停止传递。