聚合 4 规则 = 一致性边界的工程实现
一、业务问题
聚合是 DDD 战术设计中最难也最重要的模式。Vernon 在 Ch 10 给出 4 条核心规则:
- 一致性边界就是事务边界
- 优先设计小聚合
- 跨聚合用 ID 引用
- 聚合外用最终一致性
问题:工程团队做反的比例极高——要么做太大,要么做太小。
二、控制论视角
聚合的 4 规则 = 控制系统"作用域"的工程实现。
规则 1(小聚合)= 一个控制器的管辖范围小(强对位 Ch 6 §6.7)
规则 2(ID 引用)= 跨子系统通信用 ID 不用对象(强对位 Ch 21 大系统级通信)
规则 3(最终一致)= 跨子系统异步协调(强对位 Ch 21 分散控制)
规则 4(不变量)= 控制律守护的不变约束(强对位 Ch 4 §4.6 反馈决定稳定)钱学森 Ch 21 大系统理论:
"大系统的特点:不同的子系统有不同的目标……子系统间需要协调机制。"
软件对位:跨聚合 = 跨子系统;ID 引用 = 子系统间只通过 ID 通信(不深入访问对方);最终一致 = 异步协调(不等待)。
钱学森 Ch 4 §4.6:
"没有反馈,就没有自动控制"——聚合内的"反馈"是事务原子性。
钱学森 Ch 6 §6.7:
"受控对象中的相互关联并不总是有害的……有些耦合作用是有益的。"
软件对位:聚合内的"耦合"是有益的(一起保持一致);跨聚合的"耦合"应最小化。
三、4 条核心规则
规则 1:小而专(Small Aggregates)
原则:
- 优先设计小聚合
- 只包含真正需要一起保持一致的对象
- 不需要的对象放外面
原因:
- 大聚合 = 大事务 = 性能差
- 大聚合 = 锁竞争严重
- 大聚合 = 难以演化
判断"小":
- 1 个聚合根(必选)
- 0-3 个内部实体(最好)
- 0-N 个值对象(无限制)
- 整个聚合能在 1 个屏幕展示Vernon 原话:
"Consider the advantages of designing small Aggregates."
规则 2:跨聚合用 ID 引用
原则:
- 聚合 A 引用聚合 B 时,**只引用 B 的 ID**
- 不持有 B 的对象引用
- 不通过 A 修改 B
原因:
- 避免大聚合蔓延
- 跨聚合用最终一致性
- 减少耦合
实现:
# ❌ 跨聚合对象引用
class Order:
customer: Customer # 对象引用
# ✓ 跨聚合 ID 引用
class Order:
customer_id: CustomerId # ID 引用Vernon 原话:
"See why you should design Aggregates to reference other Aggregates by identity."
规则 3:聚合外用最终一致性
原则:
- 聚合内 = 强一致(ACID 事务)
- 聚合间 = 最终一致(事件 + 异步)
原因:
- 分布式事务性能差
- 业务可接受短暂不一致
- 隔离故障
实现:
[聚合 A 事务] → 发布事件 → 异步处理 → [聚合 B 更新]
例:订单创建后异步发送邮件
订单聚合内:订单创建(强一致)
邮件聚合:监听 OrderCreated 事件,异步发送规则 4:保护业务不变量(Business Invariants)
原则:
- 聚合 = 不变量的守护者
- 任何时刻不变量必须为真
- 不变量的破坏 = 业务错误
不变量例子:
- 订单总金额 = 各商品金额之和
- 已支付订单不能修改
- 库存不能为负
- 聚合内 = 强一致
- 聚合间 = 最终一致四、Tell-Don't-Ask & Law of Demeter
聚合的两个编码原则:
Tell-Don't-Ask
python
# ❌ Ask: 外部查询内部状态再决策
if order.getItems().size() > 0 and order.getStatus() == "NEW":
order.setStatus("CONFIRMED")
# ✓ Tell: 让 order 自己处理
order.confirm() # 内部判断 + 修改Law of Demeter(迪米特法则)
原则:只和直接朋友说话
- 聚合根的方法
- 根的参数
- 根自己创建的对象
- 根的全局变量
不要:
- order.getCustomer().getAddress().getCity() # ❌ 链式访问反例:
python
# ❌ 链式访问
customer_name = order.getCustomer().getName()
# ✓ 通过聚合根
customer_name = order.getCustomerName() # 内部封装五、聚合的边界判断
关键问题:
"在一次事务中,哪些对象必须一起保持一致?"
→ 答案决定聚合边界
例:
"订单创建时,订单项必须在场" → 订单 + 订单项 = 同一聚合
"支付成功后,订单状态更新" → 订单和支付 = 不同聚合(最终一致)六、工程实践
实战 1:识别聚合边界
步骤:
1. 列出业务不变量
2. 哪些不变量必须在一次事务中保持?
→ 这些对象在同一个聚合
3. 哪些不变量可异步处理?
→ 这些对象在不同聚合实战 2:避免大聚合
反模式信号:
- 一个聚合有 10+ 实体
- 修改聚合要加载大量对象
- 多人经常改同一聚合
- 锁等待严重
修复:
- 拆聚合
- 用 ID 引用
- 跨聚合用事件实战 3:实现 Tell-Don't-Ask
python
# 正例
class Order:
def __init__(self):
self.items = []
self.status = "NEW"
def confirm(self):
# 业务规则在内部
if len(self.items) == 0:
raise BusinessRuleViolation("Cannot confirm empty order")
if self.status != "NEW":
raise BusinessRuleViolation("Only NEW orders can be confirmed")
self.status = "CONFIRMED"
# 发布领域事件
self.add_event(OrderConfirmed(self.id))七、性能指标
| 指标 | 衡量 |
|---|---|
| 平均聚合大小 | 聚合平均包含几个对象(应该 < 5) |
| 跨聚合修改比例 | 多少修改跨聚合(应该高) |
| 聚合冲突率 | 同时改同一聚合的频率(应该低) |
| 不变量覆盖率 | 多少业务规则有不变量守护 |
八、边界判断
何时用大聚合:
- 业务有"必须一起改"的强规则
- 性能不是问题
- 冲突少
何时用小聚合(绝大多数):
- 性能要求高
- 多人协作
- 业务可接受最终一致
何时跨聚合用同步:
- 业务绝对不允许不一致(如金融)
- 用 Saga 模式
何时跨聚合用异步:
- 业务可接受短暂不一致
- 性能要求高
- 故障隔离重要
九、与其他 L 层方法论的关系
| 方法论 | 关系 |
|---|---|
| aggregate(基础) | 聚合基础概念 |
| tell-dont-ask-demeter | 编码原则 |
| cqrs-es | 跨聚合可用 CQRS 优化 |
| saga | 跨聚合事务 |
| coordination-deviation | 跨聚合一致性监控 |
| data-consistency(主题) | 聚合是核心 |
| dynamic-programming | 多步聚合规划 |
十、钱学森原话
Ch 6 §6.7(耦合取舍): "受控对象中的相互关联并不总是有害的……有些耦合作用是有益的。"
Ch 4 §4.6(反馈决定稳定): "没有反馈,就没有自动控制"——聚合内的"反馈"是事务原子性。
十一、总结
聚合 4 规则 = 一致性边界的工程实现:
| 规则 | 核心 |
|---|---|
| 小聚合 | 优先小,避免大聚合 |
| ID 引用 | 跨聚合用 ID,不用对象 |
| 最终一致 | 跨聚合用事件 |
| 保护不变量 | 聚合 = 不变量守护者 |
核心原则:
- 业务不变量决定聚合边界
- 小聚合 + ID 引用 = 性能 + 可演化
- 跨聚合用事件(最终一致)
- Tell-Don't-Ask 让聚合行为内聚
一句话总结:聚合 = 一致性边界,按业务不变量定义,按 4 规则实现。