4. 命令与聚合:Command 如何触发事件,Aggregate 作为事件生产者的职责

在事件风暴与领域事件建模中,命令(Command)与聚合(Aggregate)构成了事件生产的核心驱动对。命令是外部意图的载体,聚合是领域逻辑的守卫与事件的源头。理解二者如何协作,是构建可靠分布式事务的基石。

4.1 命令:领域行为的意图封装

命令(Command)是一个不可变的数据对象,它表达了“用户或外部系统希望系统做什么”。命令不是事件,它尚未发生,只是请求。在事件风暴工作坊中,命令通常以橙色便签贴在时间轴左侧,表示“触发点”。

特性 说明 示例
意图性 明确表达“谁想做什么” PlaceOrder、SubmitPayment
不可变性 创建后不可修改,保证幂等 所有字段为 readonly
单向性 命令不返回结果,只触发事件 通过消息总线发送
验证前置 进入聚合前完成基础校验 非空、格式、权限检查
实践提示:命令命名使用动词 + 名词的祈使语气,如 CancelOrder、UpdateInventory。避免使用过去时态(那是事件的命名风格)。

4.2 聚合:领域事件的唯一生产者

聚合(Aggregate)是领域驱动设计中事务一致性边界的载体。在事件驱动架构中,聚合承担着事件生产者的核心职责:它接收命令,执行业务规则,然后决定是否发布领域事件。

聚合作为事件生产者的关键原则:

  • 因果一致性:聚合内部状态的每次变更,都必须对应一个已发布的领域事件。
  • 原子性:状态变更与事件发布在同一个事务边界内完成(要么全做,要么全不做)。
  • 自包含性:聚合只发布与自己职责相关的事件,不关心谁消费。
  • 版本控制:每次事件发布都对应聚合版本号的递增,用于乐观并发控制。
核心公式:
Command + Aggregate State → Business Rules → New State + Domain Event(s)

4.3 命令到事件的完整触发流程

下图展示了命令如何通过聚合的处理,最终产生一个或多个领域事件的过程:

命令 → 聚合 → 领域事件 触发流程 Command PlaceOrder 基础验证 (非空/格式/权限) 聚合边界 (Aggregate Root) 当前聚合状态 OrderState: Pending 业务规则引擎 库存充足? 信用额度? 新状态 Confirmed 领域事件 OrderPlaced 事件总线 发布到订阅者 关键约束:状态变更与事件发布必须在同一个事务边界内完成(聚合内原子性) 聚合版本号递增,用于乐观并发控制与事件溯源

4.4 聚合生产事件的代码模式

以下是一个典型的聚合实现片段,展示命令如何触发事件:

// 聚合根:Order
public class Order : AggregateRoot
{
    public OrderId Id { get; private set; }
    public OrderState State { get; private set; }
    public Money TotalAmount { get; private set; }
    public int Version { get; private set; }

    // 私有构造函数,通过工厂方法创建
    private Order() { }

    // 处理命令:PlaceOrder
    public static Order PlaceOrder(OrderId id, List<OrderItem> items, CustomerInfo customer)
    {
        // 1. 业务规则验证
        if (items == null || items.Count == 0)
            throw new DomainException("订单必须包含至少一个商品");

        if (!customer.IsVerified)
            throw new DomainException("客户未通过验证");

        // 2. 创建聚合实例
        var order = new Order
        {
            Id = id,
            State = OrderState.Pending,
            TotalAmount = CalculateTotal(items),
            Version = 1
        };

        // 3. 发布领域事件(关键步骤)
        order.AddDomainEvent(new OrderPlacedEvent(
            orderId: id,
            customerId: customer.Id,
            totalAmount: order.TotalAmount,
            occurredAt: DateTime.UtcNow
        ));

        return order;
    }

    // 处理命令:ConfirmOrder
    public void ConfirmOrder()
    {
        if (State != OrderState.Pending)
            throw new DomainException("只有待确认的订单才能被确认");

        State = OrderState.Confirmed;
        Version++;

        AddDomainEvent(new OrderConfirmedEvent(
            orderId: Id,
            confirmedAt: DateTime.UtcNow
        ));
    }

    private static Money CalculateTotal(List<OrderItem> items)
    {
        return items.Sum(i => i.UnitPrice * i.Quantity);
    }
}
常见陷阱:不要在聚合外部手动发布事件。事件必须由聚合内部的方法(如 AddDomainEvent)收集,并在聚合保存时统一持久化。否则会导致状态与事件不一致,破坏事务原子性。

4.5 命令与事件的对比清单

在事件风暴建模中,正确区分命令与事件是避免建模混乱的关键:

维度 命令 (Command) 事件 (Event)
时态 将来时(请求发生) 过去时(已经发生)
命名 PlaceOrder OrderPlaced
可变性 不可变 不可变
生产者 用户、外部系统、定时器 聚合(唯一生产者)
消费者 聚合(命令处理器) 其他聚合、流程管理器、投影
结果 可能成功(产生事件)或失败(异常) 已发生的事实,不可撤销
持久化 通常不持久化(或仅用于审计) 必须持久化(事件溯源或事件存储)

4.6 分布式事务中的事件生产可靠性

在分布式系统中,聚合作为事件生产者必须保证至少一次交付与幂等消费。以下是两种主流实现模式:

  • 事务性发件箱(Transactional Outbox):聚合将事件写入本地数据库的 outbox 表,与业务状态在同一个本地事务中提交。后台进程读取 outbox 表并发布到消息队列。
  • 事件溯源(Event Sourcing):聚合状态完全由事件序列推导,每次命令处理只追加新事件到事件流,天然保证状态与事件一致。
生产环境建议:优先使用事务性发件箱模式,因为它对现有关系型数据库的侵入最小,且与大多数消息中间件兼容。事件溯源适合需要完整审计历史或复杂状态回溯的场景。

4.7 事件风暴中的命令与聚合建模步骤

在事件风暴工作坊中,识别命令与聚合的协作关系通常遵循以下步骤:

  1. 先找事件:在时间轴上贴出所有橙色事件便签(如 OrderPlaced、PaymentReceived)。
  2. 反推命令:对每个事件,问“什么命令导致了这件事?”贴出蓝色命令便签在事件左侧。
  3. 划定聚合:将围绕同一业务实体的一组命令与事件圈在一起,形成聚合边界(黄色便签)。
  4. 验证一致性:检查聚合内的每个命令是否都产生了对应的事件,每个事件是否都有明确的命令来源。
  5. 标注版本:在聚合边界上标注当前版本号,用于后续的并发控制设计。

通过这种结构化方法,团队可以快速建立命令与事件之间的因果映射,为分布式事务的最终一致性设计打下坚实基础。