← 返回题目列表

观察者模式中的事件对象应该如何设计?

高频 中等 第 7 / 27 题 更新于 2026/08/02
观察者模式事件对象事件建模版本兼容

简化版

事件对象应该表达已经发生的业务事实,而不是远程过程调用参数。好的事件对象通常包含事件类型、事件 id、发生时间、主体 id、版本号、最小必要数据和追踪信息。字段太少会导致监听器频繁回查,字段太多会造成耦合和兼容成本。事件一旦被多个监听器依赖,就要考虑版本演进和向后兼容。

详细版

观察者模式里,事件对象是发布方和观察者之间的契约。比如 OrderPaidEvent 不应该只是一个随便拼出来的 Map,而应该稳定表达“订单已支付”这个事实。

常见字段:

record OrderPaidEvent(
    String eventId,
    Long orderId,
    Long userId,
    BigDecimal paidAmount,
    Instant paidAt,
    long orderVersion,
    String traceId
) {}

设计事件对象要在两端取舍:

  • 只放 id:事件轻,但监听器要回查数据库;
  • 放完整快照:监听器方便,但字段膨胀、兼容成本高;
  • 放最小必要字段:通常是更稳的折中。

记忆钩子:事件对象不是“参数袋”,而是发布方和订阅方共享的业务事实契约。

完整版教学

面试提示:讲事件对象设计时,要把“业务事实、最小必要字段、版本兼容、幂等追踪”一起说出来,面试官通常会继续追问线上演进问题。

一、事件对象首先要表达业务事实

事件命名最好使用过去式或事实式:

UserRegisteredEvent
OrderPaidEvent
CouponGrantedEvent
InvoiceCreatedEvent

它表达的是“事情已经发生”。不要把事件设计成命令:

SendSmsEvent
CreateCouponEvent

这更像要求别人做某件事,耦合会更强。观察者模式更适合发布事实,让监听器自己决定是否响应。

二、事件 id 是排查和幂等的基础

事件对象应有唯一标识:

String eventId = UUID.randomUUID().toString();

事件 id 用于:

  • 日志追踪;
  • 幂等处理;
  • 重试记录;
  • 死信排查;
  • 关联多个监听器处理结果。

没有事件 id,线上排查会非常痛苦。

三、事件字段要控制粒度

三种常见设计:

设计优点缺点
只放 id轻量、兼容好监听器要回查
放完整快照监听器独立字段多、可能过期
放最小必要字段折中需要认真建模

例如短信监听器需要手机号吗?如果用户手机号可能变化,事件发生时的手机号和当前手机号可能不同。到底用哪个,要由业务语义决定。

四、事件对象不要暴露可变实体

不要直接把 ORM 实体塞进事件:

new OrderPaidEvent(orderEntity);

风险包括:

  • 实体懒加载字段在监听器里触发异常;
  • 监听器修改实体造成副作用;
  • 事务提交前后实体状态不一致;
  • 事件契约跟数据库模型强耦合。

更好的做法是使用不可变 DTO 或 record。

五、事件版本用于兼容演进

事件被多个监听器使用后,字段变化要谨慎。可以加入版本:

record OrderPaidEvent(
    String eventId,
    String eventType,
    int schemaVersion,
    Long orderId,
    BigDecimal paidAmount
) {}

新增字段通常兼容;删除字段、改字段含义、改单位都可能破坏旧监听器。跨服务事件尤其需要 schema 管理。

六、事件要带追踪信息

工程上常见字段:

traceId
tenantId
sourceApp
occurredAt
operatorId

这些字段不一定参与业务计算,但对排查、审计、灰度和多租户隔离很重要。

七、事件对象要避免两种极端

极端一:万能 Map。

Map<String, Object> event;

它看似灵活,但缺少类型约束、文档和 IDE 支持。

极端二:巨大事件。

OrderPaidEvent 包含订单、用户、商品、地址、优惠、库存全量信息

它会让发布方和所有监听器被一个庞大结构绑死。

八、常见误区与追问

  • 误区:事件对象随便放几个字段就行。 事件是发布方和订阅方的契约,字段含义要稳定。
  • 误区:直接传数据库实体最方便。 实体有懒加载、可变性、事务状态和模型耦合风险。
  • 误区:字段越全越好。 字段越多,兼容、隐私、序列化和演进成本越高。
  • 误区:事件命名用动作命令更直观。 观察者更适合发布事实,而不是命令下游执行。
  • 追问:事件里只放 id 还是放快照? 看监听器是否需要历史事实、是否能接受回查、数据是否会变化。
  • 追问:事件字段怎么演进? 新增字段优先,删除或改语义要版本化,并兼容旧消费者。
  • 追问:事件里要不要放 traceId? 生产系统建议放,方便串起发布、监听、重试和下游调用。

九、加强记忆

  1. 事件对象表达已经发生的业务事实。
  2. 必备 eventId,支撑幂等和排查。
  3. 字段粒度在只放 id 和完整快照之间取舍。
  4. 不要直接传可变 ORM 实体。
  5. 多监听器依赖后要考虑版本兼容。
  6. traceId、tenantId、occurredAt 对工程排查很重要。