L6 - 聚合(Aggregate)
聚合 = 业务不变量的守护边界——划定"在一次事务中必须保持一致"的范围。Vernon 三大规则 + 4 大突破时机 + 实施细节。
一句话
聚合 = 一致性边界 + 业务不变量的守护者——聚合根 = 唯一访问入口 + 不变量守护者 + 事务边界。小聚合 + ID 引用 + 跨聚合最终一致。
这本书在这一层说了什么
本层基于 IDDD Ch 10 完整精读(85KB / 13075 词)。
Vernon Ch 10 开篇 Piaget 引言:
"The universe is built up into an aggregate of permanent objects connected by causal relations that are independent of the subject and are placed in objective space and time."
Vernon 对聚合的判断:
"Clustering Entities and Value Objects into an Aggregate with a carefully crafted consistency boundary may at first seem like quick work, but among all DDD tactical guidance, this pattern is one of the least well understood."
核心论点 5 个:
3 大规则:
- Rule 1: Model True Invariants in Consistency Boundaries
- Rule 2: Design Small Aggregates
- Rule 3: Reference Other Aggregates by Identity
4 大突破规则的时机: 4. Reason 1: User Interface Convenience 5. Reason 2: Lack of Technical Mechanisms 6. Reason 3: Global Transactions 7. Reason 4: Query Performance
是什么
1. 为什么需要聚合
Vernon 关键洞察:
"True invariants are about consistency. They must be enforced within the boundary."
不变约束例子:
- 订单总价 = 所有订单项之和(不变约束)
- 用户邮箱 = 注册时的邮箱
- 库存数量 ≥ 0
- BacklogItem 必须属于一个 Product
反例(无聚合):
- 订单 + 订单项跨表 → 无法原子保证总价 = 各项之和
- 用户 + 权限跨服务 → 无法原子保证权限一致性
聚合的必要性:把"必须一起改"的对象放进同一事务边界 = 聚合。
2. 第一次尝试(Vernon 反例):大聚合灾难
反例(大聚合):
Scrum Aggregates = Product + BacklogItem + Task + Release + Sprint
问题:
1. 锁整个聚合 → 并发崩溃
2. 多个用户同时编辑同一 Product 的不同 BacklogItem → 锁冲突
3. 一个 BacklogItem 修改要加载整个 Product → 性能灾难
4. 测试必须启动整个聚合 → 测试慢Vernon 关键判断:
"The large-cluster Aggregate design was a bad idea."
3. 第二次尝试:多聚合正确划分
正确设计(多聚合):
Product(聚合根)
- productId, name, description, productOwnerId
BacklogItem(聚合根)
- backlogItemId, summary, storyPoints, status
Release(聚合根)
- releaseId, name, date
跨聚合:用 ID 引用(不持有对象)Vernon 关键洞察:
- 每个聚合只包含一个根实体 + 必要的值对象
- 跨聚合只用 ID 引用(不持有对象引用)
4. Rule 1: Model True Invariants in Consistency Boundaries
Vernon 原话:
"Invariants are business rules that must always be consistent."
关键:不变约束必须在一次事务中检查。
Vernon 案例:
业务规则:"Product 必须有 productOwner"
错(跨聚合):
Product 聚合 + productOwner(Owner BC 内)
→ 无法原子保证
对(用 ID 引用):
Product 聚合只引用 productOwnerId
→ "必须有 owner" 规则通过 Product 创建时强制(必须传 ownerId)反例(Vernon 反面教材):
- 业务规则"BacklogItem 必须属于 Product" → BacklogItem 引用 productId → 创建时强制
- 不要把 Product 和 BacklogItem 放进同一聚合(违背 Rule 2)
5. Rule 2: Design Small Aggregates
Vernon 原话:
"Design small Aggregates. Lean toward modeling your Aggregate with one Entity as the Aggregate Root."
小聚合的 4 大好处:
1. 性能:少对象 = 少加载 + 少锁
2. 并发:少冲突 = 高吞吐
3. 测试:少依赖 = 易测
4. 演化:少耦合 = 易改Vernon 的设计原则:
- 默认:聚合根 + 1-2 个值对象
- 偶尔:聚合根 + 1-2 个子实体
- 罕见:聚合根 + 多子实体(大聚合需谨慎)
6. Don't Trust Every Use Case(不要被用例迷惑)
关键陷阱:
Vernon:"Use cases can mislead your modeling."
反例:用户用例"创建 Product 时同时创建初始 BacklogItem" → 误导开发者把 BacklogItem 塞进 Product 聚合。
正确做法:
- 业务上"必须原子"才放同一聚合
- 业务上"先后发生可以" → 跨聚合 + 最终一致
判断标准:
- 两个对象在所有用例中必须一起改 → 同聚合
- 两个对象在某些用例中独立改 → 跨聚合
7. Rule 3: Reference Other Aggregates by Identity
Vernon 原话:
"Reference other Aggregates by identity. Favor Value Object references over direct object references."
两种引用对比:
对象引用(错):
class Order {
Customer customer; // 持有对象
}
ID 引用(对):
class Order {
CustomerId customerId; // 只持 ID
}ID 引用的 4 大好处:
1. 解耦:聚合不加载对方 → 边界清晰
2. 性能:少 JOIN / 少加载
3. 并发:少锁
4. 演化:对方改 schema 不影响8. Model Navigation(模型导航陷阱)
反模式:
// 错:深度导航修改
order.getCustomer().getAddress().setCity("New City");正确做法:
// 对:通过领域行为
order.changeShippingAddress(new Address(...));Vernon 原话:
"Model navigation is usually not needed when referencing other Aggregates."
9. Scalability and Distribution(可扩展性 + 分布式)
小聚合 + ID 引用 → 天然分布式:
- 聚合可独立部署(无跨聚合事务)
- 聚合可独立扩容
- 跨聚合用事件通信
Vernon 关键洞察:
"When modeling Aggregates that live in different processes or different machines, the rule about referencing by identity becomes even more important."
10. Rule: Use Eventual Consistency Outside the Boundary
Vernon 原话:
"Use eventual consistency outside the Aggregate boundary."
为什么:
- 聚合内:强一致(ACID 事务)
- 聚合间:最终一致(事件 + 补偿)
Vernon 关键论断:
"Within a single Aggregate boundary, consistency can be enforced. Between Aggregates, eventual consistency must be employed."
Ask Whose Job It Is(关键提问):
"Ask whose job it is."
例子(Product 创建 + BacklogItem 创建):
- Product 聚合:创建 Product + 发 ProductCreated 事件
- BacklogItem 聚合:监听 ProductCreated 事件 + 创建初始 BacklogItem
关键洞察:跨聚合的业务规则 = 业务流程(domain event flow)。
11. 突破规则的 4 大时机
Vernon 的诚实承认:
"There are reasons to break the Aggregate rules."
Reason 1: User Interface Convenience
场景:UI 一个表单需要同时改多个聚合 → 用户期望一次保存。
解决方案:
- 应用服务编排(不是聚合内事务)
- UI 层分步操作
警告:
"But this is never the right reason to make your Aggregates larger."
Reason 2: Lack of Technical Mechanisms
场景:公司没有事件机制 / 没有 Saga 框架 → 无法做最终一致。
解决:先建立技术机制(事件总线 + Saga 框架)。
Reason 3: Global Transactions
场景:业务上确实必须强一致(如金融交易)。
解决:
- 用 2PC / Saga 混合
- 或重新审视业务规则(真的必须强一致吗?)
Reason 4: Query Performance
场景:聚合过小导致查询需要 JOIN 多次 → 性能差。
解决:
- 用 CQRS(读写分离)
- 不要为了查询性能破坏聚合边界
Vernon 的关键警告:
"Don't break the Aggregate rules for query performance. Use CQRS instead."
12. Discovery 过程
Vernon 关键洞察:
"Aggregates are discovered through a process of refinement."
Discovery 4 步:
1. First Attempt:大聚合(贪图简单)
2. 遇到问题(性能 / 并发 / 测试)
3. Second Attempt:小聚合(按规则)
4. 反复迭代(Common Usage Scenarios + Estimating Aggregate Cost)Common Usage Scenarios:
- 列出所有用例
- 每个用例触及哪些对象?
- 不变约束在哪里?
Estimating Aggregate Cost:
- 加载成本(多少对象)
- 锁成本(多少冲突)
- 测试成本(多少依赖)
13. 实施细节
Create a Root Entity with Unique Identity
public class Product extends ConcurrencySafeEntity {
private ProductId productId;
protected Product() { /* ORM 需要 */ }
public Product(ProductId productId, String name, String description) {
this.productId = productId;
this.name = name;
// 业务不变约束校验
}
}Favor Value Object Parts
原则:能用值对象不用实体(值对象无 ID = 不可变 = 简单)。
public class BacklogItem extends ConcurrencySafeEntity {
private ProductId productId; // 值对象
private StoryPoints storyPoints; // 值对象
// ...
}Tell, Don't Ask + Law of Demeter
反例(Ask):
// 错:暴露内部
backlogItem.getTask().setStatus(TaskStatus.DONE);
// 然后 backlogItem 重算状态正确(Tell):
// 对:行为内聚
backlogItem.markTaskAsDone(taskId);Vernon 原话:
"Favor Tell, Don't Ask."
LoD(Law of Demeter):
"Only talk to your immediate friends."
Optimistic Concurrency
public abstract class ConcurrencySafeEntity extends Entity {
private long version;
public void incrementVersion() {
this.version++;
}
}机制:
- UPDATE WHERE version = X → 影响行数 = 0 则冲突
- 抛 OptimisticConcurrencyException
- 调用方决定重试或回滚
Avoid Dependency Injection
Vernon 原话:
"Avoid Dependency Injection in your Aggregates."
原因:
- 聚合应该是纯领域对象(POJO)
- 依赖注入会让聚合难测试
- 仓储可在应用层注入并传入聚合方法
// 错:聚合依赖仓储
public class Product {
@Autowired
private ProductRepository repository;
}
// 对:仓储由调用方注入
public class ProductService {
@Autowired
private ProductRepository repository;
public void changeProductName(ProductId id, String newName) {
Product product = repository.findById(id);
product.changeName(newName);
repository.save(product);
}
}为什么
为什么不变约束必须在聚合边界内?
根因:业务规则 = 不变约束。如果不变约束跨聚合 → 无法在事务中保证。
例子:"订单总价 = 订单项之和"
- 如果 Order + OrderItem 同聚合 → 一次事务即可保证
- 如果 Order + OrderItem 跨聚合 → 跨聚合事务 → 性能 + 锁灾难
Vernon 关键论断:
"True invariants are about consistency. They must be enforced within the boundary."
为什么聚合要小?
根因 1:性能
- 大聚合 = 一次加载多个对象 = 网络/IO 大
- 大聚合 = 锁多个对象 = 并发低
根因 2:复杂度
- 大聚合 = 多业务规则 = 测试难
- 大聚合 = 多变更原因 = 修改影响大
Vernon 关键论断:
"Small Aggregates are easier to reason about, design, test, and tune for performance."
为什么用 ID 引用其他聚合?
根因 1:解耦
- 对象引用 → 聚合知道对方结构 → 改对方破坏自己
- ID 引用 → 聚合不知道对方结构 → 各自独立演化
根因 2:性能
- 对象引用 → 加载时级联加载对方
- ID 引用 → 按需加载(懒加载)
根因 3:分布式
- 对象引用 → 跨进程引用困难
- ID 引用 → 跨进程 / 跨机器天然支持
为什么聚合间用最终一致?
CAP 定理:分布式系统不能同时保证 C + A + P。
- 强一致(2PC)→ 牺牲可用性
- 最终一致(事件)→ 牺牲强一致性,但保证可用性
Vernon 关键论断:
"Eventual consistency is not a weakness. It's a strength because it enables scalability."
怎么做
Step 1:识别不变约束
操作:
1. 与业务专家工作坊
2. 列出所有业务规则
3. 标记哪些是"不变约束"(必须始终一致)
4. 每个不变约束找到它涉及的对象
产出:
- 不变约束清单(带涉及对象)Step 2:划分候选聚合
操作:
1. 把不变约束涉及的对象聚在一起
2. 每个聚类 = 一个候选聚合
3. 验证:聚合内事务能否强制不变约束
4. 检查:聚合是否过大(> 5 对象 / 多个实体)?
产出:
- 候选聚合清单(带包含对象)Step 3:应用 ID 引用规则
操作:
1. 跨聚合的所有引用改为 ID
2. 引入值对象包装 ID(ProductId 不是 Long)
3. 聚合根方法只接受 ID 参数(不是对象)
产出:
- 聚合边界清晰(不持有对方对象)Step 4:识别跨聚合业务流
操作:
1. 列出所有用例
2. 标记跨聚合的用例
3. 每个跨聚合用例设计领域事件流
4. 考虑 Saga / 补偿
产出:
- 领域事件流图
- Saga 定义Step 5:实施并发控制
操作:
1. 聚合根继承 ConcurrencySafeEntity
2. 加 version 字段
3. 仓储保存时检查 version
4. 配置全局异常处理(OptimisticConcurrencyException → 重试)
产出:
- 乐观并发机制Step 6:写聚合测试
测试类型:
1. 单元测试:聚合行为(不依赖仓储)
2. 集成测试:聚合 + 仓储
3. 并发测试:模拟并发修改 → 验证乐观锁
原则:
- 聚合测试无 Spring 上下文(POJO 测试)
- 行为测试 > 状态测试什么时候用 / 什么时候不用
用聚合的 2 大场景
| 场景 | 关键判断 |
|---|---|
| 有不变约束需要事务保护 | 必须聚合,否则业务规则无法原子保证 |
| 业务对象有清晰生命周期 + 身份 | 实体 + 聚合根的形态 |
不用聚合(或小聚合)的 2 大场景
| 场景 | 原因 |
|---|---|
| 简单 CRUD 无不变约束 | 单实体即可,无需聚合 |
| 性能极致要求 + 数据可重建 | 聚合是开销,考虑事件溯源 |
看不见的地方
失效模式 1:大聚合(Vernon First Attempt 重演)
症状:聚合包含 5+ 实体 → 并发崩溃 + 性能差。
根因:贪图简单(把所有关联对象塞一起)。
检测:聚合根加载时关联对象 > 3 → 大聚合嫌疑。
修复:按不变约束重新划分聚合。
失效模式 2:对象引用代替 ID 引用
症状:聚合持对方对象 → 改对方破坏自己。
根因:未理解 ID 引用的解耦价值。
修复:所有跨聚合引用改 ID 值对象。
失效模式 3:跨聚合强一致尝试
症状:用 2PC / 分布式锁协调跨聚合 → 性能灾难。
根因:未接受最终一致。
修复:跨聚合用事件 + Saga。
失效模式 4:聚合内注入仓储
症状:聚合被 Spring 容器管理 → 测试必须启动 Spring → 测试慢。
根因:不理解聚合 = 纯领域对象。
修复:聚合无依赖;应用层注入仓储并传入。
失效模式 5:用例驱动破坏聚合边界
症状:为了 UI 便利合并聚合 → 聚合过大。
根因:未识别业务不变约束的真实边界。
修复:UI 编排跨聚合用例(应用层组合),不合并聚合。
失效模式 6:聚合无并发控制
症状:并发修改同一聚合 → 后写覆盖先写 → 数据丢失。
根因:无乐观锁。
修复:version 字段 + 仓储检查 + 异常重试。
失效模式 7:聚合返回内部可变对象
症状:聚合返回 getItems() 返回内部集合 → 调用方修改集合 → 不变约束崩溃。
根因:未封装。
修复:返回只读集合 + 修改通过聚合方法。
相关节点
骨架 A — 世界观
| 节点 | 关联 | 解释 |
|---|---|---|
| wonder-02-emergence-action | 强 | 聚合 = "涌现作为"的事务边界——一次业务作为的最小单元 |
| wonder-03-emergence-time | 强 | 聚合 = "时"的不变约束——生命周期内必须一致 |
| wonder-04-encounter | 中 | 跨聚合用 ID 引用 = "照面"的远距离——不直接持有,只看 ID |
骨架 B — 工程实践
| 节点 | 关联 | 解释 |
|---|---|---|
| value-stream | 中 | 聚合 = 价值流的事务节点——每聚合是一段价值 |
| change-management | 中 | 聚合边界 = 变更边界——一聚合修改不波及其他 |
| quality-assurance | 中 | 乐观并发 = 质量保障——并发不破坏数据一致性 |
与 IDDD 其他层对位
| 层 | 关系 |
|---|---|
| L5 实体与值对象 | 聚合根是特殊的实体 |
| L7 战术深入 | 工厂用于聚合创建 / 仓储用于聚合加载 |
| L8 跨 BC 集成 | 跨 BC 也用聚合的最终一致原则 |
与 EDM 对位
| EDM | IDDD L6 |
|---|---|
| Ch 2 Single Writer | 聚合根 = Single Writer 边界 |
| Ch 7 状态物化 | 聚合状态 = 物化的业务视图 |
| Ch 8 Saga | 跨聚合业务流 = Saga |
与控制论对位
聚合边界 = 控制论的不变量守护边界:
- 维纳《控制论》:系统通过负反馈维护不变量
- IDDD 聚合:通过事务边界维护不变量
- 两者都是"在边界内强制不变量,在边界外允许变化"
状态
L6 概念递进落地完成(基于原书 ch10 85KB / 13075 词)。
- 7 个核心论点(3 大规则 + 4 大突破规则时机 + Discovery + 实施)
- First Attempt/Second Attempt 反例 + 正例对比
- Tell-Don't-Ask + LoD + 乐观并发 + 避免 DI 全部覆盖
- 7 个失效模式
- 骨架 A/B + IDDD + EDM + 控制论 4 重共振映射