观察者模式中的事件对象应该如何设计?
简化版
事件对象应该表达已经发生的业务事实,而不是远程过程调用参数。好的事件对象通常包含事件类型、事件 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? 生产系统建议放,方便串起发布、监听、重试和下游调用。
九、加强记忆
- 事件对象表达已经发生的业务事实。
- 必备 eventId,支撑幂等和排查。
- 字段粒度在只放 id 和完整快照之间取舍。
- 不要直接传可变 ORM 实体。
- 多监听器依赖后要考虑版本兼容。
- traceId、tenantId、occurredAt 对工程排查很重要。