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 命令到事件的完整触发流程
下图展示了命令如何通过聚合的处理,最终产生一个或多个领域事件的过程:
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 事件风暴中的命令与聚合建模步骤
在事件风暴工作坊中,识别命令与聚合的协作关系通常遵循以下步骤:
- 先找事件:在时间轴上贴出所有橙色事件便签(如
OrderPlaced、PaymentReceived)。 - 反推命令:对每个事件,问“什么命令导致了这件事?”贴出蓝色命令便签在事件左侧。
- 划定聚合:将围绕同一业务实体的一组命令与事件圈在一起,形成聚合边界(黄色便签)。
- 验证一致性:检查聚合内的每个命令是否都产生了对应的事件,每个事件是否都有明确的命令来源。
- 标注版本:在聚合边界上标注当前版本号,用于后续的并发控制设计。
通过这种结构化方法,团队可以快速建立命令与事件之间的因果映射,为分布式事务的最终一致性设计打下坚实基础。