观察者模式中的推模型和拉模型有什么区别?
简化版
推模型是主题通知观察者时直接把事件数据传过去,观察者拿到数据就能处理;拉模型是主题只通知“发生变化”,观察者再主动向主题查询需要的数据。推模型简单直接,拉模型更灵活但耦合和查询成本更高。
详细版
观察者模式里,主题通知观察者时有两种常见数据传递方式。
推模型:
- 主题把事件数据主动传给观察者;
- 观察者不需要再查询主题;
- 适合事件数据明确、字段不太重的场景。
拉模型:
- 主题只告诉观察者“状态变了”;
- 观察者根据需要再去主题或其他服务查询数据;
- 适合数据量大、观察者需求差异很大的场景。
工程里更常用的是推模型,尤其是业务事件,例如订单支付成功事件会携带订单号、用户 ID、支付金额等关键信息。但如果事件数据非常大,或者观察者需要的数据差异很大,可以只推 ID,让观察者按需拉取。
完整版教学
一、推模型是什么
推模型的意思是:事件发布时,把观察者处理所需的数据一并推过去。
例如订单支付成功:
public class OrderPaidEvent {
private Long orderId;
private Long userId;
private BigDecimal amount;
}
观察者收到事件后直接处理:
public void onOrderPaid(OrderPaidEvent event) {
sendSms(event.getUserId(), event.getAmount());
}
推模型的优点是简单、直观、减少二次查询。缺点是事件对象可能越来越大,因为不同观察者想要的字段不一样。
二、拉模型是什么
拉模型的意思是:主题只通知事件发生,观察者自己去拉取需要的数据。
例如事件里只放订单 ID:
public class OrderPaidEvent {
private Long orderId;
}
观察者收到事件后再查询订单服务:
Order order = orderService.getById(event.getOrderId());
拉模型的优点是事件轻量,观察者可以按需获取数据。缺点是可能带来额外查询、接口依赖和一致性问题。
三、两种模型的核心取舍
推模型像“把资料一起发给你”,拉模型像“告诉你资料更新了,你自己去查”。
推模型适合:
- 事件字段比较明确;
- 大多数观察者都需要相同数据;
- 希望减少查询;
- 希望事件可审计、可重放。
拉模型适合:
- 数据量很大;
- 不同观察者需要的数据差异明显;
- 数据变化快,事件只适合放主键;
- 发布方不想承载太多字段。
四、真实项目经常是混合模型
很多系统不会纯粹推或纯粹拉,而是采用混合方式:事件中推送核心字段,观察者必要时再拉详细数据。
例如:
事件推送:orderId、userId、amount、paidAt
观察者按需拉取:商品明细、收货地址、营销标签
这样既保证常用信息可直接处理,又避免事件对象过度膨胀。
五、事件字段设计要避免两个极端
第一个极端是事件太瘦,只有一个 ID。所有观察者都去查数据库或远程服务,容易造成查询风暴。
第二个极端是事件太胖,把订单、用户、商品、优惠、地址、库存信息全部塞进去。事件变得难维护,也可能暴露不该暴露的数据。
比较稳的设计是:事件携带“证明这个事件发生所需的核心事实”,其他扩展信息按需查询。
六、事件信息量与一致性窗口
推模型携带 8 个字段,每个观察者都收到约 1 KB;拉模型只推 1 个 id,但 5 个观察者可能额外产生 5 次查询。
push: Subject -> full event; pull: Subject -> id -> observers query source
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 选择推或拉,本质是在事件稳定性、负载和观察者自主性之间做权衡。 |
| 适用边界 | 推太多会扩大契约和传输成本,推太少会造成 N 次查询及读取时状态已变化;工程上常用 id 加关键快照的混合模型。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:选择推或拉,本质是在事件稳定性、负载和观察者自主性之间做权衡。
八、常见误区与追问
- 误区:拉模型一定能读到事件发生时的状态。 观察者查询时源对象可能已变化;需要历史快照时必须在事件中携带版本或快照。
- 误区:发布事件天然等于异步和最终一致性。 观察者只定义一对多通知关系;同步还是异步、是否跨进程以及一致性语义都由具体实现决定。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:事件对象应包含哪些字段? 至少包含身份、发生时间或版本及处理所需稳定事实,避免塞入整个可变聚合对象。
- 追问:观察者失败后由谁重试? 模式本身没有答案;同步场景要定义异常传播,异步场景要由执行器、事件总线或消息系统提供重试与死信策略。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
推模型是“主题把数据推给观察者”,拉模型是“主题只通知变化,观察者自己查数据”。工程上常用混合方案:事件里放核心字段和主键,复杂详情按需拉取,避免事件过瘦或过胖。