实体、值对象、聚合与聚合根
# 实体、值对象、聚合与聚合根
这四个概念是 DDD 战术设计的地基,也是领域模型的基本组成单元。这篇合并整理了课程第 4、5 讲的内容。
# 一、实体:靠 ID 认人
实体(Entity)的核心特征是有唯一标识。属性怎么变都无所谓,只要 ID 不变,它就还是它。
好比一个人,从小到大身高体重相貌全变了,但身份证号没变,他还是他。系统里的商品也一样:价格、库存、描述天天改,但商品 ID 不变,就始终是同一个商品。
实体在不同阶段的形态:
| 阶段 | 形态 |
|---|---|
| 领域建模时 | 领域模型中承载属性和行为的业务对象 |
| 写代码时 | 实体类,充血模型——业务逻辑写在实体方法里,不是贫血的 getter/setter |
| 运行时 | DO(领域对象),带唯一 ID |
| 持久化时 | 映射到数据库表,不一定一对一 |
最后一点值得展开:DDD 是先建领域模型再考虑持久化,所以实体和表的关系很灵活:
- 1 对 1:大多数情况
- 1 对多:权限实体 = user 表 + role 表组合而来
- 多对 1:为避免联表,客户实体和账户实体的数据存同一张表
- 1 对 0:纯内存计算的实体(比如根据价格配置算出来的折扣对象),根本不落库
# 二、值对象:一堆属性打包成一个概念
值对象(Value Object)没有 ID,通过属性值判断相等,且不可变。
最经典的例子是地址。人员实体里如果直接平铺"省、市、区、街道"四个字段,属性就很零碎;把它们打包成一个 Address,作为整体挂在人员实体上,概念就完整了:
public class Person { // 实体
private String id; // 唯一标识
private String name;
private Address address; // 值对象:整体引用,无 ID
}
public class Address { // 值对象
private String province;
private String city;
private String county;
private String street;
}
2
3
4
5
6
7
8
9
10
11
12
值对象不可变——地址变了不是"改地址",而是换一个新的 Address 对象整体替换。
# 值对象怎么存数据库?
这是值对象最实用的地方:简化库表设计。传统范式设计里地址得单独建表再外键关联;用值对象后有两种嵌入方式:
| 方式 | 做法 | 适用 | 代价 |
|---|---|---|---|
| 属性嵌入 | 地址字段直接放进人员表 | 单条记录的值对象 | 值对象多了表会臃肿 |
| 序列化大对象 | 多个地址序列化成 JSON 存一个字段 | 一对多的值对象(多个收货地址) | 没法按属性条件查询 |
所以用之前先想清楚:这个值对象将来要不要被搜索? 要按地址搜人,就别用 JSON 大字段。
# 实体还是值对象?看场景
同一个东西在不同上下文里的身份会变:
- 收货地址:只是描述订单的特征,整体替换 → 值对象
- 行政区划维护系统里的地址:要单独增删改查,有生命周期 → 实体
判断口诀:在意"是谁"用实体,在意"是什么值"用值对象。
# 三、聚合:一起干活的对象打包管理
实体和值对象都是"个体",而聚合(Aggregate)就是把业务上紧密关联的实体和值对象组合成的"组织",保证它们协作时数据一致。
聚合的几个关键属性:
- 聚合是数据修改和持久化的基本单元,一个聚合对应一个仓储(Repository)
- 聚合内部高内聚,聚合之间松耦合——照这个思路拆出来的微服务天然"高内聚低耦合"
- 聚合是领域模型里最底层的边界,微服务架构演进时以聚合为单位拆分组合
业务逻辑写在哪,按层级分:
单个实体的逻辑 → 实体方法(充血模型)
跨实体(同聚合) → 领域服务
跨聚合 → 应用服务
2
3
# 四、聚合根:组织的负责人 + 对外接口人
聚合根(Aggregate Root)首先是个实体,同时是聚合的管理者。它有三重身份:
- 实体本身:有自己的属性和业务行为
- 内部管理者:协调聚合内的实体和值对象按业务规则干活
- 对外接口人:外部想访问聚合内的东西,必须先经过聚合根——聚合之间只通过聚合根 ID 关联,不搞直接对象引用
为什么要这个"接口人"?如果任何外部对象都能随便改聚合内的实体,数据一致性没法保证;上锁又伤性能。收口到聚合根,规则就有人管了。
# 五、怎么设计一个聚合(五步法)
以保险投保场景为例:
- 事件风暴找出所有实体和值对象:投保单、标的、客户、被保人……
- 找聚合根。判断标准:有独立生命周期?有全局唯一 ID?能创建/修改其它对象?有专门模块管它?→ 投保单、客户是聚合根
- 围绕聚合根聚类:按单一职责和高内聚,把紧密依赖的对象划进来 → 形成"投保聚合"和"客户聚合"
- 画出依赖关系:比如投保聚合里的"投保人"是从客户聚合冗余来的值对象——客户数据以后变了,也不影响已生成的投保单(这是业务上合理的快照语义)
- 归入限界上下文:多个聚合按业务语义划进同一个限界上下文
# 六、聚合设计五原则
出自《实现领域驱动设计》,用大白话讲:
- 聚合内封装真正的不变业务规则——不是随便把对象凑一堆,而是这些对象必须遵守同一套规则
- 设计小聚合——聚合太大,实体多、并发冲突多、重构难;小聚合更抗业务变化
- 聚合之间只用 ID 引用——不要对象直接引用,否则边界模糊、耦合上升
- 聚合内强一致,聚合间最终一致——一个事务只改一个聚合;跨聚合的修改走领域事件异步处理
- 跨聚合调用走应用层——别让领域服务跨聚合调用,也别跨聚合联表,为将来拆分留后路
原则是通用的,但落地时如果碰到性能要求、全局事务这些现实约束,突破原则也没问题——一切以解决实际问题为出发点。
# 七、四个概念一张表
| ID | 可变性 | 生命周期 | 相等性判断 | 角色 | |
|---|---|---|---|---|---|
| 实体 | 有(聚合内唯一) | 状态可变 | 由聚合根管理 | 按 ID | 业务对象 |
| 值对象 | 无 | 不可变,整体替换 | 无,用完即扔 | 按属性值 | 描述实体特征 |
| 聚合根 | 有(全局唯一) | 状态可变 | 独立 | 按 ID | 聚合管理者、对外接口 |
| 聚合 | —— | —— | —— | —— | 数据一致性边界、持久化单元 |