跳到内容

10.2 事件通知、状态行为与集中协调

Observer、State 与 Mediator 分别解决:一个事实怎样通知多个关注者、对象行为怎样随生命周期变化,以及一组同级对象怎样避免互相形成网状依赖。

Observer:发布事实,不遥控订阅者

Observer 建立一对多通知。发布者只知道观察者契约,不知道每个观察者为何响应。

java
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 强引用可能延长其生命周期。比“全部换成弱引用”更可靠的做法,是让订阅返回显式句柄:

java
interface Subscription extends AutoCloseable {
    @Override void close();
}

拥有者在生命周期结束时关闭订阅。弱引用可能让 listener 在无人持有强引用时悄悄消失,也不替代明确所有权。

State:把状态特有行为与转换放在一起

当一个对象在不同状态下允许的操作和结果差异很大,散落的 if (status == ...) 会让转换规则难以维护。

java
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、条件更新或锁:

sql
UPDATE registration
   SET state = :next_state, version = version + 1
 WHERE id = :id AND version = :expected_version;

受影响行数为 0 表示状态已被其他事务推进,应重新读取并按幂等规则处理。

状态少、转换封闭时,枚举加一个集中 switch 可能比类层次更清楚。State 适合状态特有行为较多、分支在多处重复的情况。

Mediator:集中一组对象的协作协议

一个比赛房间中,倒计时、参赛者连接、裁判面板和直播状态若互相直接调用,会形成网状依赖。Mediator 让它们只与协调者通信:

java
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

参考资料

Built with VitePress | Software Systems Atlas