关于 aggregate-design
面向实践 DDD 的开发者,介绍如何划分集约并维持其不变条件。内容涵盖集约根访问、按 ID 引用其他集约,以及以结果一致性处理边界外的关系。
主要功能
- 01以不变条件划分集约边界
- 02通过集约根访问内部对象
- 03按 ID 引用其他集约
- 04保持集约规模精简
- 05以领域事件协作并实现结果一致性
适用场景
- 01评估集约边界是否过大
- 02检查外部访问是否经过集约根
- 03审查跨集约事务设计
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ j5ik2o/ okite-ai --skill aggregate-design安装后在 Claude Code 中输入/aggregate-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsontrigger-eval.jsonreferences/python.mdrust.mdscala.mdtypescript.md详细介绍
聚合设计指南
DDD 中聚合(Aggregate)的设计原则。
什么是聚合
聚合 = 一致性边界
- 聚合是一个对象图,并将整个对象图作为一个单元处理
- 聚合内部维护真正的不变量(必须始终满足的约束)
- 聚合根(根实体)是整个聚合的唯一入口点
边界示例:Car 聚合
┌─────────────────────────────────────┐
│ Car集約 │
│ ┌──────────────────────────────┐ │
│ │ Car (集約ルート) │ │
│ │ - carId: CarId │ │
│ │ - tires: List[Tire] │ │
│ │ - engine: Engine │ │
│ └──────────────────────────────┘ │
│ ↓ 所有 ↓ 所有 │
│ ┌──────────┐ ┌─────────┐ │
│ │ Tire │ × 4 │ Engine │ │
│ │ (VoかEnt)│ │ (VoかEnt)│ │
│ └──────────┘ └─────────┘ │
└─────────────────────────────────────┘
- Tire 和 Engine 位于 Car 聚合边界内
- 外部无法直接访问 Tire 或 Engine
- 只能通过 Car 进行操作
Design by Contract(DbC)
聚合应基于契约进行设计。
| 契约 | 说明 | 责任 |
|---|---|---|
| 前置条件(Precondition) | 调用方法前必须满足的条件 | 调用方 |
| 后置条件(Postcondition) | 方法执行后必须满足的条件 | 实现方 |
| 不变量(Invariant) | 必须始终满足的条件 | 实现方 |
各语言的详细实现模式请参阅 references/typescript.md、references/scala.md、references/rust.md、references/python.md。
基本原则
1. 推荐不可变性(Immutability)
在现代开发中,推荐使用不可变(Immutable)对象。除非有特别理由,否则应保持不可变。 更新状态时,沿用现有值,仅覆盖需要更改的字段。 这样可以避免添加字段时遗漏修改,并明确表达更新意图。
| 语言 | 不可变更新模式 |
|---|---|
| TypeScript | Props 类型 + ...this.props 展开语法 |
| Scala | case class + copy() |
| Rust | struct + ..self 结构体更新语法 |
| Python | dataclass(frozen=True) + replace() |
2. 强一致性边界
聚合是一个强一致性边界。聚合内部的所有状态都由该聚合管理。
3. 对其他聚合进行间接引用
聚合内部不保存对其他聚合的引用。需要与其他聚合建立关联时,通过 ID 进行间接引用。
4. 完整构造函数
使用基本构造函数初始化所有状态。即使使用重载,也必须将其设计为使用基本构造函数的辅助构造函数。
5. 防御性复制
如果持有可变对象,向外部返回时必须返回副本,或将其转换为不可变对象。
6. 维护不变量
无论执行何种操作,都不得进入无效状态。对于不可变聚合,应通过基本构造函数进行保护。
结构原则
7. 通过聚合根访问
访问聚合内部的实体或值对象时,必须通过聚合根。
8. 1 个事务 = 1 个聚合
不要在单个事务中更新多个聚合。通过最终一致性确保聚合间的一致性。
9. 保持聚合精简
过大的聚合会导致并发问题(锁竞争)。只包含真正需要保持一致的范围。
协作原则
10. 通过领域事件进行协作
聚合状态发生变化时发布领域事件,其他聚合或外部系统订阅事件并作出响应。
11. 乐观锁(仅在有需求时)
仅在需要检测并发更新冲突时才使用版本号。没有相关需求时则无需使用。
12. 持久化无关性
聚合应专注于领域逻辑,不关心如何保存(这是仓储的职责)。
Evans 规则(Eric Evans)
源自 DDD 原著的聚合规则。
Evans 规则 1:根实体的职责
只有聚合根可以直接从仓储获取。
- 外部对象不能持有对聚合内部的引用
- 聚合根可以传递内部实体的标识符,但仅限临时使用
- 聚合根负责维护整个聚合的不变量
Evans 规则 2:限制对内部对象的访问
边界内的实体拥有局部标识符,只需在聚合内唯一即可。
- 外部无法直接访问
- 内部实体的标识符无需全局唯一
Vernon 的 4 条规则(Vaughn Vernon)
源自《实践领域驱动设计》的设计规则。
规则 1:将真正的不变量纳入一致性边界
Design aggregates based on true invariants(在一致性边界内确保真正的不变量)
良い例:Order集約
- 注文合計 = 各明細の小計の合計(真の不変条件)
- この計算は即座に正しくなければならない
悪い例:結果整合性で十分な場合
- 在庫数の更新は別の集約で結果整合性
规则 2:保持聚合精简
Keep aggregates small(聚合应保持精简)
大型聚合的问题:
- 锁竞争增加
- 并发能力下降
- 事务失败率上升
❌ Bad: 大きな集約
Product集約
├── productId
├── name
├── backlogItems: List[BacklogItem] ← 数百件になる可能性
└── ...
✅ Good: 分割した集約
Product集約 BacklogItem集約
├── productId ├── backlogItemId
├── name ├── productId(IDで参照)
└── ... └── ...
规则 3:通过 ID 引用其他聚合
Reference other aggregates by ID only(其他聚合仅通过 ID 引用)
与前面提到的“对其他聚合进行间接引用”原则相同。
规则 4:最终一致性
Use eventual consistency outside the boundary(边界外采用最终一致性)
- 通过领域事件 + 最终一致性确保聚合间的一致性
- 不要在单个事务中更新多个聚合
审查清单
审查现有聚合时使用。
Design by Contract
- 是否明确指定了前置条件(require)?
- 是否满足后置条件(ensuring)?
- 是否在构造函数及每次操作后维护了不变量?
基本原则
- 聚合是否不可变(Immutable),或者是否有明确理由无法做到不可变?
- 是否持有对其他聚合的直接引用(是否仅使用 ID)?
- 是否提供了完整构造函数?
- 辅助构造函数是否使用了基本构造函数?
- 返回可变对象时,是否进行了防御性复制?
- 是否在构造函数中验证了不变量?
Evans 规则
- 是否只有聚合根可以直接从仓储获取?
- 是否不存在从外部直接引用聚合内部的情况?
- 内部实体的标识符是否仅限临时使用?
Vernon 规则
- 聚合边界内是否仅包含真正的不变量(是否存在过度包含)?
- 聚合是否足够精简(是否包含数百条记录的集合)?
- 是否仅通过 ID 引用其他聚合?
- 是否通过最终一致性确保聚合间的一致性?
结构原则
- 外部访问是否仅通过聚合根进行?
- 是否存在可以直接操作内部实体的漏洞?
- 聚合大小是否合适(是否过大)?
协作原则
- 状态变化时是否发布领域事件?
- 有乐观锁需求时,是否实现了版本号?
- 没有乐观锁需求时,是否避免使用不必要的版本号?
- 持久化逻辑是否泄漏到了聚合内部?
相关技能(建议一并阅读)
使用此技能时,请同时参阅以下技能:
domain-building-blocks:构成聚合的要素(值对象、实体、领域服务)的设计aggregate-transaction-boundary:聚合与事务边界的关系(1 个事务 = 1 个聚合规则)cross-aggregate-constraints:聚合间约束检查与最终一致性的设计repository-placement:聚合仓储接口的放置位置