加载中...

本篇要搞懂的 2 个概念:聚合(Aggregate)、聚合根(AggregateRoot)。

承接前文:04 讲学了实体值对象——领域模型的"基础积木"。05 讲再往上一层:把基础积木按业务规则组合起来,形成业务一致性的最小单元,这就是聚合;而聚合的"负责人"叫聚合根

到本讲为止,DDD 的层级体系就完整了:

领域:整个业务(02 讲)
  ↓ 拆分
子域:核心域 / 通用域 / 支撑域(02 讲)
  ↓ 圈定语义边界
限界上下文:通用语言的"地盘"(03 讲)
  ↓ 按业务规则组合
聚合:业务一致性的最小单元(05 讲)  ← 本讲
  ↓ 由谁负责
聚合根 + 实体 + 值对象(04 + 05 讲)

聚合(Aggregate)

为什么需要聚合?

04 讲学了实体值对象——但只有它们还不够。很多业务场景需要多个对象协同完成一件事

  • 电商下单时要同时:创建订单、生成订单项、记录收货地址、扣减库存、计算金额
  • 投保时要同时:创建投保单、记录被保人、记录投保人、关联报价规则

如果让这些对象"各自为政",谁都能改谁,很容易出现数据不一致——比如订单标成"已支付"了,但订单项还没扣减库存,这就是 bug。

聚合就是为了解决这个问题:把业务上必须一起变化、必须保持一致的实体和值对象捆成一个整体来管理

聚合的定义

聚合是由业务紧密关联的实体和值对象组合而成的领域对象集合,是 DDD 里数据修改和持久化的基本单元

"基本单元"是这个定义的核心词,它意味着:

操作 行为
修改 要改就整个聚合一起改,不能只改一半
加载 从数据库读取时,整个聚合一起加载
保存 持久化时一次性写入,不会出现"主表存了、从表漏存"的情况
事务一致性 强一致性事务只在聚合内部保证;跨聚合用最终一致性

技术上,一个聚合对应一个仓储(Repository)——DDD 中持久化的统一入口,对外屏蔽底层是数据库、文件还是别的存储。

聚合的两个关键特征

  1. 有一个聚合根(AggregateRoot):聚合对外的唯一接口人,所有进出聚合的请求都必须经过它(下一节细讲)。
  2. 有一个清晰的边界:根据"业务紧密度"圈出来——哪些对象属于这个聚合、哪些不属于。

聚合之间是松耦合的:跨聚合协作只能通过聚合根 ID 引用,不能直接对象引用。按这种方式设计出来的微服务,自然就是"高内聚、低耦合"。

🪞 辅助理解:把整个 DDD 类比为一家公司——

DDD 层级 公司里对应什么
实体 / 值对象 一个个员工
聚合 一个部门(员工的组织单位)
聚合根 部门经理
限界上下文 整个事业部

部门内的员工天天协作、共享资料(聚合内强一致);跨部门协作必须走经理对接,而不是底下员工互相串门(聚合间通过 ID 引用、最终一致)。这就是"高内聚、低耦合"在组织上的体现。

一个具体例子:电商订单聚合

订单聚合(Order Aggregate)
├── 订单(聚合根,有订单号 ID)
├── 订单项(实体,记录每个商品的购买信息)
├── 收货地址(值对象)
├── 支付金额(值对象)
└── 下单时间(值对象)

这个聚合体现了所有特征:

  • 统一加载:查一个订单时,订单 + 订单项 + 地址一起加载进内存。
  • 统一修改:要改地址,得通过订单聚合的"修改地址"方法——订单自己知道哪些数据要一起变。
  • 统一持久化:保存时一次性写入,绝不会"订单存了但订单项漏存"。
  • 对外只能 ID:用户聚合要查这个订单,只能拿订单 ID 来查,不能直接绕过订单去访问订单项。

换个角度再走一遍订单例子(直觉版)

如果上面的定义还是飘,用这个角度再过一遍——聚合到底在防什么

一个订单里装着这些东西:

订单(订单ID、状态、总金额)        ← 实体(聚合根)
 ├─ 订单项1(商品、数量、单价)      ← 实体(订单内部的)
 ├─ 订单项2(商品、数量、单价)      ← 实体
 └─ 收货地址(省市县街道)          ← 值对象

为什么要把它们圈成一坨? 因为它们之间有一条必须同时成立的规则

「订单总金额 = 所有订单项的(单价 × 数量)之和」

如果外部能绕过订单、单独去改某个订单项的数量,订单总金额就对不上了——数据就不一致了。聚合存在的全部意义,就是堵死这种"绕过去乱改"的口子。

聚合根 = 这台机器唯一的「门」。 这里订单就是聚合根,规矩是:外部想动聚合里任何东西,必须经过订单。

  • ✅ 想加一个订单项?必须调 订单.增加订单项(...),由订单在内部顺手把总金额重算了。
  • ❌ 不允许外部直接拿到「订单项2」偷偷改数量。订单项藏在订单后面,外面够不着。

一个关键判断:什么该圈进同一个聚合?
问自己——「这两个东西,必须在同一个事务里一起改、一起保证一致吗?」

  • 订单 ↔ 订单项 → (改订单项必然要重算金额)→ 同一聚合
  • 订单 ↔ 商品 → 不是(下单后商品涨价,不该影响已下的订单)→ 拆成两个聚合,订单里只存 商品ID

经验法则:聚合尽量小。圈太大,每次存取一个订单要拖一堆东西,还容易并发打架(呼应后面的"设计小聚合"原则)。

聚合在 DDD 分层中的位置

聚合属于 DDD 的领域层——领域层包含多个聚合,共同实现核心业务逻辑。业务逻辑写在哪一层,看它的粒度:

业务粒度 实现位置
单个实体内的逻辑 实体类的方法里(充血模型)
同一聚合内、跨多个实体的逻辑 领域服务(Domain Service)
跨多个聚合的逻辑 应用服务(Application Service)

聚合根(AggregateRoot)

定义:聚合根也称为根实体,它既是实体,又是聚合的管理者

为什么需要聚合根?

传统数据模型里每个实体都是对等的,如果允许实体之间无控制地互相调用、互相修改,就会导致数据不一致;用锁来防止又会增加复杂度、降低性能。

聚合根的存在,是为了用统一的业务规则保障聚合内数据一致性

聚合根的三重身份

身份 职责
作为实体本身 拥有实体的属性和业务行为,实现自身的业务逻辑
作为聚合的管理者 在聚合内部协调实体和值对象按固定规则协同工作
作为对外接口人 接受外部任务、对外暴露聚合根 ID,外部不能绕过它直接访问内部实体

一条关键规则

聚合之间通过聚合根 ID 关联引用。如果需要访问另一个聚合的实体:必须先访问对方的聚合根,再由它导航到内部。外部对象不能直接访问聚合内的实体

🪞 辅助理解:聚合根就像部门经理——既是部门的一员(实体),也是部门的管理者(管理实体和值对象)。

别的部门要找你部门的人办事,必须先经过经理;不能直接绕过经理去找具体员工——否则就乱套了。

怎样设计聚合?

DDD 用事件风暴来设计聚合,5 步法

步骤 做什么 产出
1 采用事件风暴,梳理业务行为中所有的实体和值对象 一组散落的领域对象
2 从众多实体中选出聚合根 1 个或多个聚合根
3 找出与聚合根关联紧密的实体和值对象,构成聚合 聚合(含 1 个聚合根 + N 个实体 + N 个值对象)
4 画出聚合内对象的引用和依赖关系 聚合内部结构图
5 多个聚合按业务语义划入同一个限界上下文 限界上下文

怎么判断一个实体是不是聚合根?

按四个特征判断:

  1. 是否有独立的生命周期
  2. 是否有全局唯一 ID
  3. 是否可以创建或修改其它对象
  4. 是否有专门的模块来管理它

四个条件同时满足,基本就是聚合根了。

一个完整例子:投保业务场景

按 5 步法走一遍:

Step 1:事件风暴梳理出实体和值对象——投保单、标的、客户、被保人、报价单、报价规则……

Step 2:找聚合根——投保单客户符合"独立生命周期 + 全局 ID + 可创建修改其它对象 + 有专门模块管理",是两个聚合根。

Step 3:构建两个聚合——

聚合 聚合根 内部对象
客户聚合 客户 客户基本信息、联系方式等
投保聚合 投保单 报价单(实体)→ 报价规则(子实体)、投保人(值对象)、被保人(值对象)、标的(值对象)

Step 4:画引用关系——投保单引用报价单实体,报价单又引用报价规则子实体。

关键细节:投保人和被保人在投保聚合里是值对象——数据是从客户聚合冗余过来的(通过客户 ID 关联取来)。即使未来客户聚合的数据变了,也不会影响投保单上的这些值对象——因为投保单上的是"那一刻的快照"。

Step 5:投保聚合和客户聚合按业务语义放进同一个"投保限界上下文"。

🪞 辅助理解:聚合之间的"冗余"很像电商订单上的"收货地址"——

  • 用户改了"个人地址簿"里的地址(客户聚合改)→ 已经下单的订单不会跟着变(投保单上的值对象不变)
  • 因为订单上记的是下单那一刻的快照,不是个引用

再用订单走一遍 5 步法(更贴近日常的例子)

投保场景偏保险业务,下面换成电商下单,把同样的 5 步再走一遍,体会一下"流程是通用的"。

Step 1 · 事件风暴梳理对象

把下单这件事涉及的领域对象全列出来(先不管谁管谁):

订单、订单项、商品、收货地址、支付金额、下单时间、用户、优惠券……

Step 2 · 挑聚合根(套四个判断)

候选 独立生命周期 全局 ID 能创建/修改其它对象 有专门模块管理 结论
订单 ✅ 订单号 ✅ 创建并管理订单项 ✅ 订单服务 聚合根
商品 ✅ 商品 ID ✅ 商品服务 聚合根(另一个聚合的)
用户 ✅ 用户 ID ✅ 用户服务 聚合根(另一个聚合的)
订单项 ❌ 依附订单 聚合内唯一即可 普通实体,归订单管

→ 关键判断:订单项离开订单没有任何意义(不会有人单独查一条"订单项"),所以它不是聚合根,归到订单聚合内部。

Step 3 · 圈聚合

聚合 聚合根 内部对象
订单聚合 订单 订单项(实体)、收货地址(值对象)、支付金额(值对象)、下单时间(值对象)
商品聚合 商品 商品基本信息、库存、价格等
用户聚合 用户 用户资料、地址簿等

→ 边界判断用上一节那条规则:「改它必须和订单一起改、保证一致吗?」 订单项改了要重算订单总金额 → 圈进来;商品涨价不该影响已下订单 → 留在商品聚合外面。

Step 4 · 画引用关系

  • 订单聚合内部:订单 → 订单项(直接对象引用,订单导航到订单项)。
  • 订单聚合对外:订单项里只存 商品ID,不存整个商品对象;收货地址是下单那一刻从用户地址簿冗余过来的值对象(快照)。
订单聚合
  订单(订单号)
   ├─ 订单项[ 商品ID(→商品聚合)、数量、单价 ]   ← 跨聚合只留 ID
   ├─ 收货地址(值对象,下单快照)               ← 用户改地址簿不影响它
   └─ 支付金额、下单时间(值对象)

→ 这正好踩中"用户改了地址簿、已下订单不跟着变"的快照逻辑,和投保单上的被保人值对象是同一回事。

Step 5 · 划入限界上下文

订单聚合归「交易/订单上下文」,商品聚合归「商品上下文」,用户聚合归「用户上下文」。跨上下文协作只通过聚合根 ID + 应用层组合,不做跨库 JOIN。

对照看:投保例子里"投保单"对应这里的"订单",“报价单"对应"订单项”,“被保人/投保人值对象"对应"收货地址值对象”——结构完全同构,换的只是业务名词。这就是 5 步法可复用的地方。

聚合的 5 个设计原则

来自《实现领域驱动设计》,原文有点绕,逐条解释:

1. 在一致性边界内建模真正的不变条件

聚合用来封装真正的不变性,不是简单地把对象组合在一起。聚合内有一套不变的业务规则,所有实体和值对象都遵守它;边界之外的东西与聚合无关

→ 这是"高内聚"的根本来源。

2. 设计小聚合

聚合太大会导致:

  • 实体太多 → 内部管理复杂
  • 高频操作时并发冲突或数据库锁 → 系统可用性下降
  • 业务变化时不得不重构整个聚合

小聚合则更灵活,更能适应业务变化。

🪞 辅助理解:聚合做大了就像一个超级部门管太多业务——开会拖沓、改动牵动全员、版本发布也慢。拆小后每个部门职责清晰,迭代独立。

3. 通过唯一标识引用其它聚合

聚合之间只能通过聚合根 ID 关联,不能直接对象引用。

如果把外部聚合的对象直接放进聚合边界内管理,会导致:

  • 聚合的边界不清晰
  • 聚合之间耦合度上升

4. 在边界之外使用最终一致性

  • 聚合内:数据强一致(一次事务搞定)
  • 聚合间:数据最终一致(异步补齐)

一次事务中最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态变化,应当用领域事件异步驱动(这是 06 讲的内容)。

🪞 辅助理解:强一致 vs 最终一致就像夫妻两个人买东西——夫妻两个人买(同一个聚合内)必须当场对账;隔壁邻居买东西(跨聚合)只要"晚上算账时对得上"就行,不必每一分钟都完全同步。

5. 通过应用层实现跨聚合的服务调用

为了实现聚合之间的解耦、以及未来以聚合为单位拆分微服务,避免

  • 跨聚合的领域服务调用
  • 跨聚合的数据库表 JOIN

跨聚合的协同应该走应用服务(Application Service)这一层。

总览:一句话浓缩 5 个原则

🪞 辅助理解:5 个原则其实就一句话——“聚合内紧紧抱团,聚合外松松握手”。

  • 抱团:聚合内强一致、直接对象引用、共享一个仓储、统一规则
  • 握手:聚合外通过 ID 引用、最终一致、应用层组合、不 JOIN

需要注意:这些原则不是教条。当遇到性能瓶颈、技术能力限制、全局事务管理等现实约束时,可以突破——一切以解决实际问题为出发点。

总结

聚合是 DDD 领域模型中由业务紧密关联的实体和值对象组成的领域对象集合,是数据修改和持久化的基本单元——每一个聚合对应一个仓储。聚合有一个聚合根作为对外入口、有一个明确的边界保证内部高内聚、与其他聚合之间松耦合。在 DDD 分层架构中,聚合属于领域层,承担核心业务逻辑:聚合内部的逻辑写在实体方法里(充血模型),跨实体的写领域服务,跨聚合的走应用服务。

聚合根是聚合的"负责人",既是实体也是管理者。它有三重身份:作为实体拥有自己的属性和行为;作为管理者协调聚合内实体和值对象按规则协同;作为对外接口人通过 ID 接受外部请求。一条不能破的规则:聚合之间只能通过聚合根 ID 互相引用,外部不能直接访问聚合内的实体——这是保证聚合边界清晰、数据一致性的关键。

怎样设计聚合遵循 5 步法(事件风暴梳理对象 → 选聚合根 → 圈聚合 → 画引用关系 → 划入限界上下文)。判断聚合根看四个特征:独立生命周期、全局 ID、能创建/修改其它对象、有专门模块管理。一个完整聚合就是"1 个聚合根 + N 个实体 + N 个值对象"的组合,多个聚合再按语义聚到一个限界上下文里。

5 个设计原则可以浓缩成一句话——聚合内紧紧抱团(强一致、对象引用、统一仓储),聚合外松松握手(ID 引用、最终一致、应用层组合)。这套约束让聚合在业务上保证一致性、在技术上方便拆分微服务。

先把"是什么"回答清楚

概念 官方定义(DDD 标准说法) 一句话大白话 例子
聚合(Aggregate) 业务紧密关联的实体和值对象组合而成的领域对象集合,是数据修改和持久化的基本单元 一个"部门"——把功能相关的实体和值对象组织起来 投保聚合(投保单 + 报价单 + 投保人 + 被保人)
聚合根(AggregateRoot) 聚合的根实体;既是实体本身,又是聚合的管理者和对外接口 部门经理——既是组员,也是负责人 投保单(投保聚合的聚合根)

四种领域对象的差异对比

维度 聚合 聚合根 实体 值对象
本质 一组对象的集合 一个特殊的实体 有 ID 的对象 无 ID 的属性集
唯一性 全局唯一 ID ID 在聚合内唯一即可 无 ID,按属性值判断相等
生命周期 包含多个对象的生命周期 独立 依附聚合根,由聚合根管理 无生命周期,用完即扔
可变性 可变(一致性修改单元) 可变 可变 不可变
持久化 对应一个仓储 是聚合的持久化入口 一般会持久化,但不一定 1:1 嵌入实体表,不单独持久化
跨聚合引用 只能用聚合根 ID 引用 只能引用聚合内的对象 尽量只引用值对象

一句话速记

聚合是"部门",聚合根是"部门经理";部门内对象直接对话,部门之间通过经理 ID 握手。

落地建议:怎么找到你的聚合?

按以下顺序实操:

  1. 事件风暴:列出业务中所有的命令、事件、实体、值对象。
  2. 挑聚合根:用四个判断(独立生命周期 / 全局 ID / 能创建修改其它对象 / 有专门模块管理)挑出聚合根候选。
  3. 圈紧密关联的对象:在每个聚合根旁边圈一圈"被它管理"或"它必须协调"的实体和值对象。
  4. 检查不变性:聚合内是否有一套"必须同时成立"的业务规则?没有的话可能切大了。
  5. 检查耦合:聚合之间是否只通过 ID 引用?JOIN 多了就要重新切。
  6. 小聚合优先:宁愿多切几个聚合,也别整一个超大聚合。

思考题

回到自己的项目:

  • 一个常见的业务流程(如下单、注册、审批)里,哪几个对象天然有"独立生命周期 + 全局 ID + 管理其它对象" 的特征?它们就是聚合根候选。
  • 你目前代码里有没有跨聚合 JOIN跨聚合直接对象引用?这些是潜在的耦合点。
  • 业务上有没有"一次操作必须同时修改多个聚合"的需求?如果有,可以思考能否拆成"先改 A 聚合 → 发领域事件 → 异步改 B 聚合"的最终一致方案(这是下一讲的预热)。
公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关