10.2 事件通知、状态行为与集中协调
Observer、State 与 Mediator 分别解决:一个事实怎样通知多个关注者、对象行为怎样随生命周期变化,以及一组同级对象怎样避免互相形成网状依赖。
Observer:发布事实,不遥控订阅者
Observer 建立一对多通知。发布者只知道观察者契约,不知道每个观察者为何响应。
public record MatchFinished(
MatchId matchId,
PlayerId winnerId,
java.time.Instant finishedAt) {}
@FunctionalInterface
public interface MatchFinishedListener {
void on(MatchFinished event);
}
public final class LocalMatchEvents {
private final java.util.List<MatchFinishedListener> listeners;
public LocalMatchEvents(java.util.List<MatchFinishedListener> listeners) {
this.listeners = java.util.List.copyOf(listeners);
}
public void publish(MatchFinished event) {
for (MatchFinishedListener listener : listeners) {
listener.on(event);
}
}
}事件名称使用已经发生的事实 MatchFinished,而不是命令式 UpdateLeaderboard。后者把发布者重新耦合到订阅者职责。
同步 Observer 的失败语义
上面的实现是同步、按顺序、同线程调用。必须决定:
- 一个 listener 失败,后续 listener 是否继续;
- listener 是否参与发布者事务;
- 是否允许递归发布;
- listener 能否修改订阅列表;
- 处理时限和观测方式是什么。
若“保存比赛”和“更新积分”必须原子完成,不应仅靠无事务保证的 Observer 假装解耦。若允许最终一致,可以在事务中写 Outbox,再由消息系统异步投递;那已经进入消息可靠性问题,需要幂等消费者、重试、顺序和死信策略。
进程内 Observer 与消息代理不是一回事:前者通常共享进程和故障,后者跨进程并引入投递语义。
订阅生命周期
动态注册时,Subject 持有 listener 强引用可能延长其生命周期。比“全部换成弱引用”更可靠的做法,是让订阅返回显式句柄:
interface Subscription extends AutoCloseable {
@Override void close();
}拥有者在生命周期结束时关闭订阅。弱引用可能让 listener 在无人持有强引用时悄悄消失,也不替代明确所有权。
State:把状态特有行为与转换放在一起
当一个对象在不同状态下允许的操作和结果差异很大,散落的 if (status == ...) 会让转换规则难以维护。
public sealed interface RegistrationState {
RegistrationState paymentConfirmed(PaymentReceipt receipt);
RegistrationState cancel(java.time.Instant now);
}
public record PendingRegistration(Deadline deadline)
implements RegistrationState {
@Override
public RegistrationState paymentConfirmed(PaymentReceipt receipt) {
return new ConfirmedRegistration(receipt);
}
@Override
public RegistrationState cancel(java.time.Instant now) {
return new CancelledRegistration(now, "cancelled before payment");
}
}
public record ConfirmedRegistration(PaymentReceipt receipt)
implements RegistrationState {
@Override
public RegistrationState paymentConfirmed(PaymentReceipt ignored) {
return this; // idempotent duplicate callback
}
@Override
public RegistrationState cancel(java.time.Instant now) {
return new RefundPendingRegistration(receipt, now);
}
}每个状态对象表达合法行为和下一状态;上下文对象持有当前状态并负责持久化转换。
转换必须与并发一致
两个进程同时处理“支付成功”和“取消”时,内存中的 State 模式不能防止丢失更新。数据库层仍需版本号/OCC、条件更新或锁:
UPDATE registration
SET state = :next_state, version = version + 1
WHERE id = :id AND version = :expected_version;受影响行数为 0 表示状态已被其他事务推进,应重新读取并按幂等规则处理。
状态少、转换封闭时,枚举加一个集中 switch 可能比类层次更清楚。State 适合状态特有行为较多、分支在多处重复的情况。
Mediator:集中一组对象的协作协议
一个比赛房间中,倒计时、参赛者连接、裁判面板和直播状态若互相直接调用,会形成网状依赖。Mediator 让它们只与协调者通信:
public final class MatchRoomCoordinator {
private final PlayerConnections players;
private final MatchClock clock;
private final BroadcastPort broadcast;
public void playerReady(PlayerId playerId) {
players.markReady(playerId);
if (players.allReady()) {
clock.start();
broadcast.publish(MatchRoomEvent.started());
}
}
}协调协议集中后,参与者变简单,也更容易测试事件顺序。代价是 Mediator 可能成长为新的 God Object。应按用例或协作上下文拆分,不要把全系统所有通信塞进一个 SystemMediator。
Mediator 与 Facade 的方向不同:Facade 为外部调用者提供简单入口;Mediator 协调内部同级对象,使它们不直接互相依赖。一个应用服务可能同时承担两种角色,但要清楚它维护的是哪套协议。
三种模式的选择问题
| 问题 | 模式 |
|---|---|
| 一个事实需要通知未知数量关注者 | Observer |
| 同一操作随对象状态改变行为 | State |
| 多个同级对象之间协作形成网状依赖 | Mediator |