06-聚合与一致性边界-DDD领域驱动设计


title: "06 聚合(Aggregate)与一致性边界"


学习目标

  • 理解聚合(Aggregate)的目的:用边界控制一致性,降低模型耦合与并发冲突
  • 能区分"聚合根(Aggregate Root)"与聚合内部对象,并落实到访问规则
  • 能用 Vaughn Vernon 的聚合设计原则解释:为什么"跨聚合强一致"往往是坏主意
  • 能把"事务边界、并发、领域事件"与聚合边界串成一个完整的设计理由链

核心概念

聚合是什么

聚合是一组对象(实体/值对象)的组合,它们在一个边界内保持一致性。聚合有两条课堂口径:

  • 外部只能通过聚合根访问聚合内对象
  • 聚合边界通常就是事务边界(一次命令一次事务,修改一个聚合)

聚合根是什么

聚合根是聚合的入口(通常是实体),负责:

  • 维护聚合内不变量(Invariant)
  • 对外暴露命令方法(例如 pay/cancel/addItem
  • 产生并收集领域事件(Domain Events)

本书示例中:Order 是订单聚合的聚合根。

课堂讲授:一致性有成本,边界是为了控制成本

如果你把太多对象放进一个"超级聚合",会发生:

  • 单事务写入范围扩大,锁竞争/并发冲突增加
  • 任何小改动都要加载巨量数据,性能下降
  • 团队协作成本上升:一个聚合成为"共享修改热点"

如果你把边界切得太碎,又会发生:

  • 需要大量跨聚合的强一致检查(最终写回到 Service 大杂烩)
  • 业务规则无法落地在模型内部(模型表达力下降)

所以聚合设计的本质是:在正确的地方"付出一致性成本",并用领域事件处理跨边界协作。

经典原则(Vaughn Vernon):聚合设计的教学版 4 条

注意:这里是教学口径的简化表述,用来指导"如何划边界",不是法律条文。

  1. 在一个事务中只修改一个聚合(一次命令 → 一次事务 → 一次聚合)
  2. 聚合尽量小:只包含维护不变量所必需的对象
  3. 聚合之间用标识关联 :用 OtherAggregateId 引用,而不是对象引用
  4. 跨聚合一致性用最终一致性:通过领域事件/流程编排完成协作

伪代码示例:订单聚合的边界形状(示例)

示例订单聚合根体现了几件关键事:

  • 不变量:MAX_ORDER_LINES = 50
  • 状态机:OrderStatus 约束状态转换(例如只有 PAID 才能 ship
  • 规约:OrderPaymentSpecification / OrderCancellationSpecification
  • 领域事件收集:domainEvents 列表(OrderPaidEvent/OrderCancelledEvent
text 复制代码
class Order {                                   // Aggregate Root
  orderNumber: OrderNumber
  customerId: CustomerId
  status: OrderStatus
  items: List<OrderItem>
  domainEvents: List<DomainEvent>

  addItem(item):
    require status.canPay()
    require items.size < 50                      // invariant
    items.add(item)

  pay():
    require OrderPaymentSpecification().isSatisfiedBy(this)
    status = status.transitionToPaid()
    domainEvents.add(OrderPaidEvent(orderNumber.value, customerId.value, totalAmount))

  cancel(reason):
    require OrderCancellationSpecification().isSatisfiedBy(this)
    status = status.transitionToCancelled()
    domainEvents.add(OrderCancelledEvent(orderNumber.value, customerId.value, reason))
}

Mermaid 图:一次命令的典型边界(单聚合 + 事件协作)

DomainEventPublisher OrderRepository Order Aggregate Application Service DomainEventPublisher OrderRepository Order Aggregate Application Service 用户/上游调用方 PayOrder(orderId) 1 findById(OrderNumber.of(orderId)) 2 Order 3 order.pay() 4 domainEvents += OrderPaidEvent 5 save(order) 6 publish(event) 7 clearDomainEvents() 8 用户/上游调用方

聚合之间如何"引用"

在 DDD 教学中,一个可复用的规则是:跨聚合只传递标识

例如,"支付聚合(Payment)"不应该直接持有 Order 对象,而应持有 OrderNumber

text 复制代码
class Payment {                 // another aggregate root
  paymentId: PaymentId
  orderNumber: OrderNumber      // reference by identity (VO)
}

这样做的直接收益:避免"意外把两个聚合放进同一事务/同一对象图"。

并发与事务:为什么"一次命令只改一个聚合"

事务边界(Transaction Boundary)

课堂建议口径:

  • 一个命令(Command)由一个应用服务方法处理
  • 这个方法在一个事务里修改一个聚合并保存
  • 跨聚合的后续动作用领域事件触发

应用服务处理 PayOrder 的典型形状是:加载 Order → 调 order.pay()save → 发布事件。

并发控制(概念)

聚合根通常会配合"乐观锁版本号(version)"做并发保护:

text 复制代码
repository.save(order):
  require order.version matches DB version
  update ... set version = version + 1

这一点在教材里需要掌握"为什么",即:

  • 聚合是并发冲突的单位
  • 聚合越大,并发冲突越频繁

常见误区

  • 误区 1:把整个业务域做成一个聚合:任何写操作都互相抢锁,系统越做越慢
  • 误区 2:跨聚合强一致检查写在应用服务里:用例编排膨胀,规则难复用,测试困难
  • 误区 3:聚合之间直接对象引用:一不小心就把多个聚合拉进同一事务
  • 误区 4:聚合根暴露内部集合可变引用:外部绕过不变量修改内部状态(建议只暴露只读视图或副本)

课堂练习

以"订单域"为例,做一次边界推理(写出答案与理由):

  1. Order 聚合里,OrderItem 应该是值对象还是实体?理由是什么?
  2. "订单 + 支付 + 发货"是否应该放在同一个聚合?如果不是,跨边界一致性怎么做?
  3. 选择一个不变量(例如"订单行数 ≤ 50"),写出它应该由谁守护:应用服务、聚合根、值对象,为什么?

自测题

  1. 为什么说"聚合边界通常就是事务边界"?
  2. Vernon 的原则中,哪一条最能直接降低并发冲突?为什么?
  3. 你能举例说明"跨聚合最终一致性"的一次典型协作吗(用事件/命令描述即可)?

延伸阅读

  • 参考资料索引:references.md
  • Vaughn Vernon:Effective Aggregate Design(含聚合设计原则)
相关推荐
一位搞嵌入式的 genius18 小时前
深入理解 JavaScript 异步编程:从 Event Loop 到 Promise
开发语言·前端·javascript
m0_5649149218 小时前
Altium Designer,AD如何修改原理图右下角图纸标题栏?如何自定义标题栏?自定义原理图模版的使用方法
java·服务器·前端
飞升不如收破烂~18 小时前
# Spring Boot 跨域请求未到达后端问题排查记录
java·spring boot·后端
AllData公司负责人18 小时前
【亲测好用】数据集成管理能力演示
java·大数据·数据库·开源
brevity_souls18 小时前
SQL Server 窗口函数简介
开发语言·javascript·数据库
阿蒙Amon18 小时前
C#每日面试题-值传递和引用传递的区别
java·面试·c#
aloha_78918 小时前
乐信面试准备
java·spring boot·python·面试·职场和发展·maven
火云洞红孩儿19 小时前
零基础:100个小案例玩转Python软件开发!第六节:英语教学软件
开发语言·python
Knight_AL19 小时前
Spring Boot 多模块项目中优雅实现自动配置(基于 AutoConfiguration.imports)
java·spring boot·mybatis
短剑重铸之日19 小时前
《RocketMQ研读》面试篇
java·后端·面试·职场和发展·rocketmq