本篇要搞懂的 2 个概念:实体(Entity)、值对象(Value Object)。
承接前文:02 拆领域 → 子域;03 在子域内圈定限界上下文。04 讲再往里走一步:限界上下文内部装着什么? 答案是实体和值对象——它们是领域模型的基础单元,也是战略设计向战术设计过渡时最关键的两个领域对象。
理解这两个概念的难点在于:它们在不同阶段(业务、代码、运行、数据库)会呈现出不同的形态。下面按原文的顺序,把每种形态都过一遍。
前置概念:什么是事件风暴?
正文里会反复出现"事件风暴"这个词,先做一段简单铺垫。
事件风暴(Event Storming) 是 DDD 战略设计的主要方法:由领域专家、产品、架构师、开发围在一面墙(或白板)前,用便利贴梳理业务全过程,逐步识别出业务里的关键对象:
| 对象 | 是什么 | 典型例子 |
|---|---|---|
| 事件(Event) | 业务里"已经发生"的事情 | 订单已支付、库存已扣减 |
| 命令(Command) | 触发事件的动作 | 下单、付款 |
| 实体(Entity) | 执行命令、承载状态的对象 | 订单、用户、商品 |
| 值对象(Value Object) | 描述实体特征的属性集 | 地址、金额、时间段 |
| 聚合(Aggregate) | 把紧密相关的实体和值对象圈成一组 | 订单聚合(含订单 + 订单项 + 收货地址) |
| 限界上下文(Bounded Context) | 把聚合按业务边界圈成更大的单元 | 投保上下文、理赔上下文 |
整个过程"先发散后收敛"——先把所有业务动作贴满墙,再聚类、划线、定边界。
🪞 理解辅助:事件风暴有点像团队一起"破案"——业务专家是目击者,开发是侦探,便利贴是线索,最后还原出业务的"案情时间线"。
它的最大价值是让团队用统一的语言(通用语言)一起看清楚业务流程,避免开发凭文档臆想、产品凭直觉表达。
本篇里反复出现的"在事件风暴中找出实体和值对象",就是这个过程里的一个具体产物——上面表格里第 3、4 行。
实体(Entity)
定义:在 DDD 中,拥有唯一标识符、且标识符在历经各种状态变更后仍能保持一致的领域对象,叫作实体。对实体而言,重要的不是属性,而是延续性和标识——这种延续性甚至会跨越软件的生命周期。
举例:商品上下文中的"商品"是一个实体,通过商品 ID 唯一标识。无论价格、名称、上下架状态如何变化,只要 ID 不变,它就是同一个商品。
🪞 理解辅助:可以把实体类比为"一个人"——身份证号是唯一标识,无论改名、搬家、变胖变瘦、年龄增长,都是同一个人。系统在乎的是身份证号,不是当下的发型或体重。
反过来如果某个对象没有"身份证号"概念、也不需要追踪它"是谁",那它八成不是实体,而是值对象。
实体的四种形态:先看一张全景图
同一个实体在 DDD 设计的不同阶段,"长什么样"是不一样的——就像同一个人,在户口本上、工资条上、身份证上、组织架构图上呈现的"形态"都不同,但都是同一个人。
| 形态 | 看的角度 | 长什么样 |
|---|---|---|
| 业务形态 | 领域专家 | 事件风暴白板上写着名字的便利贴(如"订单") |
| 代码形态 | 开发 | 一个 Java/Python 类,带字段和方法 |
| 运行形态 | 系统跑起来 | 内存里一个带 ID 的对象实例 |
| 数据库形态 | DBA | 一张表里的一行(也可能多行、来自多张表) |
下面一个个看。
1. 实体的业务形态
实体是领域模型中的核心对象,是多个属性、操作和行为的载体。
在事件风暴中,可以根据命令、操作或事件,找出产生这些行为的业务实体;再按业务规则,将依存度高、关联紧密的实体和值对象聚类,形成聚合(下一讲细讲)。
直观感受:电商场景的事件风暴白板上,会贴出这样的便利贴序列——
- 命令「下单」 → 谁在执行?谁记录这件事?→ 实体「用户」「订单」
- 事件「订单已支付」 → 谁的状态变了?→ 实体「订单」(状态从"未支付"变成"已支付")
在这一步,"实体"就是被识别出来、贴在白板上、写着名字的那张便利贴——它是业务视角下"做事 / 被记账"的对象。实体和值对象一起构成领域模型的基础单元。
🪞 辅助理解:业务形态就像剧本里写着的"主角张三"——还没演到台上、还没穿戏服,但他已经是这个故事的核心角色,所有情节都围绕他展开。
2. 实体的代码形态
实体在代码中表现为实体类,类里包含属性和方法。
DDD 中的实体类通常采用充血模型:与该实体相关的所有业务逻辑都封装在实体类的方法里;跨多个实体的领域逻辑则放在领域服务中实现。
充血 vs 贫血对比:很多传统项目实体只是数据容器,所有业务逻辑都堆在 Service 里——这叫"贫血模型",DDD 不推荐。
// ❌ 贫血模型:实体只是 getter/setter 集合
class Order {
private String orderId;
private OrderStatus status;
// 只有 getter/setter
}
class OrderService {
public void pay(Order order) {
if (order.getStatus() == OrderStatus.UNPAID) {
order.setStatus(OrderStatus.PAID);
}
}
}
// ✅ 充血模型(DDD 推荐):业务规则写在实体方法里
class Order {
private String orderId;
private OrderStatus status;
public void pay() {
if (status != OrderStatus.UNPAID) {
throw new IllegalStateException("订单状态不允许支付");
}
this.status = OrderStatus.PAID;
}
}
核心区别:贫血模型里 Service 知道太多业务规则,实体只是字段袋;充血模型里实体自己保护自己的规则,Service 只做"协调",不做"业务"。
🪞 辅助理解:充血模型像"动作片演员自己会武术"——打戏自己上、规则自己懂;贫血模型像"演员只摆 pose、所有打斗交给替身"。在贫血模型里,Service 就是那个全场救火、什么活都干的替身。
3. 实体的运行形态
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一 ID。可以对它进行多次修改,修改前后数据可能大不相同,但只要 ID 相同,就仍然是同一个实体。
内存中的样子:
修改前: 修改后:
Product { Product {
id: "SKU-001", id: "SKU-001" ← ID 不变
name: "红色 T 恤" → name: "全棉舒适 T 恤"
price: 99 price: 199
stock: 100 stock: 0
} }
改了名字、改了价格、库存清零——只要 ID 还是 SKU-001,系统就当它是同一个商品。这就是"延续性"在运行时的具体表现。
🪞 辅助理解:就像同一个人,从婴儿到成年,外貌、身高、能力全都变了,但身份证号没变——户籍系统就认定他还是同一个人。运行形态的实体也是这样:内存里的数据可以变天翻地覆,只要 ID 没变,它就还是"那个对象"。
4. 实体的数据库形态
DDD 与传统的"数据模型优先"设计相反:先构建领域模型,再映射到数据持久化对象。
实体与持久化对象的对应关系不是固定的 1:1:
| 关系 | 说明 | 举例 |
|---|---|---|
| 1:1 | 最常见的情况 | 一个商品实体对应一张商品表 |
| 1:0 | 实体只在内存中临时存在,不持久化 | 基于多个价格配置实时算出的折扣实体 |
| 1:N | 一个实体由多张表组合得到 | 权限实体由 user 表和 role 表组合 |
| N:1 | 多个实体共享一张表 | 为提升性能,把客户信息和账户信息保存到同一张表,客户、账户两个实体从同一持久化对象中生成 |
怎么理解非 1:1 的关系?
传统 ORM 思维默认"一个实体一张表",DDD 不这么僵化——映射关系由业务决定,不由数据库范式决定:
- 1:N:业务上要看到完整的"权限"概念,但底层是
user表 +role表 → 内存里组装成一个权限实体。 - N:1:业务上明明是客户、账户两个对象,但 JOIN 性能差 → 干脆存到一张表里,用的时候拆出两个实体。
- 1:0:业务上有个"折扣"概念,每次基于实时配置算出来 → 用完就丢,不落库。
关键认知:实体是为业务服务的,表是为存储和性能服务的,两者不必一一对应。
🪞 辅助理解:实体和表的关系,就像"人"和"档案"的关系——
- 一个人可能同时有人事档案、医疗档案、税务档案(1:N)
- 几个人也可能合用一份联合档案(N:1)
- 临时来访者甚至根本没档案(1:0)
档案怎么存是档案室的事,但"这个人是谁"始终是业务层关心的事。
值对象(Value Object)
定义(出自《实现领域驱动设计》):通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体,用来描述领域的某个特定方面,没有标识符。
值对象有两个关键特征:
- 不可变:当度量或描述改变时,用另一个值对象整体替换,而不是修改原对象。
- 可做相等性比较:两个值对象的属性值完全相同,就视为相等;并且不会对协作对象产生副作用。
通俗讲:值对象本质上是一个集合——若干用于描述目的、具有整体概念、不可修改的属性。它在领域建模中保证属性归类的清晰和概念的完整性,避免实体属性零碎。
🪞 理解辅助:值对象可以类比为"名片上印着的信息"——名字、电话、地址。这些信息没有独立的"地址 ID"或"姓名 ID",你不会说"这个地址搬家了",而是说"这个人换了一个新地址,整张名片重印"。
同理,“金额”(数额 + 货币)、“时间段”(开始 + 结束)、“坐标”(经度 + 纬度)都是典型值对象——它们都是描述性的属性集,没有独立身份。
例:人员实体原本有"姓名、年龄、性别 + 省、市、县、街道"等属性,地址相关属性零碎。把"省、市、县、街道"打包成"地址值对象"后,人员实体只引用一个"地址"概念,业务表达更清晰。
值对象的四种形态:先看一张全景图
值对象的四种形态和实体类似,但侧重点不同——值对象始终是依附于实体的描述,所以四种形态都围绕"如何被实体引用"展开:
| 形态 | 看的角度 | 长什么样 |
|---|---|---|
| 业务形态 | 领域专家 | 实体身上一组"描述性属性集"(如订单上的收货地址) |
| 代码形态 | 开发 | 实体的一个字段,或一个无 ID 的 Class |
| 运行形态 | 系统跑起来 | 嵌进实体的两种方式:平铺字段 or 序列化 JSON |
| 数据库形态 | DBA | 通常不单独建表,挤在实体表里 |
1. 值对象的业务形态
值对象与实体一样来源于事件风暴,包含若干属性,与实体一起构成聚合。
与实体对照来看:
| 实体 | 值对象 | |
|---|---|---|
| 本质 | 看得见、摸得着的业务对象 | 若干属性的集合 |
| 是否含业务行为 | 有业务属性、业务行为、业务逻辑 | 只有数据初始化和有限的不涉及修改的行为,基本不含业务逻辑 |
| 逻辑归属 | 独立的业务对象 | 物理上独立,逻辑上仍是实体属性的一部分,用于描述实体特征 |
少数标准类型的值对象有自己的限界上下文和持久化对象,可建立共享数据类微服务(如数据字典)。
🪞 辅助理解:值对象就像语法里的"形容词"——它不能独立存在,必须依附于名词(实体)。
“红色的"单独说没意义,但可以修饰"苹果”、“汽车”、“衣服”;“北京市朝阳区 XX 路"单独说也只是个标签,必须搭在"用户的地址”、"公司的注册地"上才有意义。
2. 值对象的代码形态
值对象在代码中有两种形态:
- 单一属性的值对象:直接定义为实体类的属性。
- 属性集合的值对象:定义为 Class,将多个属性归集到一起,没有 ID,被实体整体引用。
代码示例:
class Person { // 实体(有 ID)
private String id; // 主键
private String name; // 单一属性值对象
private Integer age; // 单一属性值对象
private Address address; // 属性集合值对象(无 ID)
}
class Address { // 值对象:没有 ID
private final String province;
private final String city;
private final String district;
private final String street;
// 构造函数初始化,所有字段 final,不可变
// 没有 setter——要改只能 new 一个新的 Address 整体替换
}
关键观察:
Person有id→ 实体Address没有id、所有字段final→ 值对象Address整体作为Person的一个字段,“被引用"而不是"被外键关联”
🪞 辅助理解:单一属性值对象像一个"形容词"(
name描述"叫什么"、age描述"多大");属性集合值对象像"一组形容词的打包"——Address就是把"省 + 市 + 县 + 街道"这四个零碎描述打包成"地址"这个完整概念,让Person一次性引用。
3. 值对象的运行形态
值对象嵌入实体有两种方式:
| 方式 | 适用场景 | 实现 |
|---|---|---|
| 属性嵌入 | 单一属性的值对象 or 只有一条记录的多属性值对象 | 把值对象拆成几个字段,平铺在实体表中 |
| 序列化大对象 | 一条或多条记录的多属性值对象(如一个人多个通讯地址) | 把值对象序列化成 JSON,整体嵌入实体的一个字段 |
值对象创建后不允许修改,只能用另一个值对象整体替换。
两种方式对比:
属性嵌入——地址平铺在 Person 表里:
Person 表
| id | name | age | province | city | district | street |
|----|------|-----|----------|------|----------|--------|
| 1 | 张三 | 30 | 北京 | 北京 | 朝阳 | xx 路 |
序列化大对象——多个地址序列化成 JSON 存进一个字段:
Person 表
| id | name | age | addresses |
|----|------|-----|------------------------------------------------|
| 1 | 张三 | 30 | [{"province":"北京","city":"北京",...},{...}] |
选择依据:
- 值对象只有一份且经常拿来用 → 属性嵌入(查询方便,可以建索引)
- 值对象有多份且整体使用 → 序列化大对象(节省表数量,但难按内部字段查询)
🪞 辅助理解:
- 属性嵌入像把地址直接印在名片正面——每个字段一目了然,想看哪个字段就看哪个。
- 序列化大对象像把多张地址条折好塞进名片背面的口袋——拿到时要先打开 JSON,但能装好几张。
4. 值对象的数据库形态
DDD 引入值对象的一个重要目的:从"数据建模为中心"转向"领域建模为中心"——减少数据库表的数量、减少表间复杂依赖、简化设计、提升性能。
传统数据建模严格遵循范式:一个实体对应一张表,主表关联 N 张从表。值对象的数据库设计大多不严格遵循范式:值对象属性值和实体属性值保存在同一张数据库表中。
以人员 + 地址为例,有三种设计方案:
| 方案 | 做法 | 问题 |
|---|---|---|
| A | 把地址所有属性直接铺到人员表 | 破坏地址作为整体的业务含义和概念完整性 |
| B | 拆成人员表 + 地址表两张表 | 增加了不必要的实体和表,处理表间关系增加了复杂度 |
| C(DDD 推荐) | 领域建模时把地址设为值对象(保留概念完整性);数据建模时把地址嵌入人员表(不引入额外表) | 兼顾业务含义和数据库简洁 |
也有 DDD 专家更激进:要发挥对象的威力,就要优先领域建模、弱化数据库的作用——只把数据库当作保存数据的仓库,即便违反数据库设计原则,只要业务能顺利运行就没问题。
🪞 辅助理解:值对象的"嵌入"思想,就像把叉子、刀、勺子当作"餐具套装"整体保存——而不是建三张表(叉子表、刀表、勺子表)再用外键关联。
"套装"作为一个业务概念是有意义的(你买的是一套,不是单独的叉子),但数据库不需要为这个"套装"建独立的表,只要在主表里留一组字段或一个 JSON 字段就够了。
5. 值对象的优势与局限
值对象是一把双刃剑:
| 优势 | 局限 |
|---|---|
| 简化数据库设计、减少表数量 | 难以按值对象内部属性快速查询(序列化方式尤甚) |
| 提升数据库性能(少 JOIN) | 实体引用过多值对象时,会堆积一堆缺乏概念完整性的属性,反而失去业务含义 |
| 保留业务概念完整性 | 复杂查询场景下不灵活 |
经验:
- 业务上只读、整体使用、很少独立查询 → 适合值对象(如收货地址、订单金额)
- 业务上频繁修改、独立查询、关系复杂 → 改用实体(如行政区划中维护的地址)
实体和值对象的关系
实体和值对象是微服务底层最基础的对象,共同实现实体最基本的核心领域逻辑。
两者在某些场景下可以互换,是设计成实体还是值对象,取决于对象在当前业务场景中扮演的角色:
- 被某一实体引用、只承担描述实体的作用、值只能整体替换 → 设计为值对象(如订单中的收货地址)
- 经常被独立修改、作为独立对象存在 → 设计为实体(如行政区划中维护的地址信息)
值对象的引入,也回应了 DDD 的一个根本主张:领域建模优先于数据建模。传统做法(一个实体一张表)容易让领域模型被数据模型绑架;值对象让领域建模可以更灵活地映射到数据库,而不必为了数据库范式去切碎业务概念。
🪞 总结类比:实体是"一个人"——关心的是这个人是谁、有什么经历。值对象是"这个人名片上的描述"——名字、电话、地址。
同一段"地址"信息:
- 印在订单收据上 → 值对象(用完就扔、整体替换)
- 印在政府区划表里 → 实体(要常常维护、有行政区划编码)
不是地址本身决定了它是什么,是它在哪张"桌子上"决定的。
总结
实体是 DDD 领域模型里有唯一标识符的对象,标识在状态变更后保持不变。它的核心特征是"延续性"——属性可变、状态可变,但 ID 不变,所以始终是同一个对象。实体在不同阶段有四种形态:业务上是领域模型的核心对象、聚合的组成单元;代码上是充血模型的实体类,业务逻辑写在方法里;运行时是带 ID 的 DO;数据库上和持久化对象不是固定 1:1,可能是 1:0、1:N、N:1。
值对象是 DDD 领域模型里没有标识符、通过属性值识别的对象,将多个相关属性组合为一个概念整体。它的核心特征是"不可变"——一旦创建不能修改,要变只能整体替换。值对象在代码上有两种形态:单一属性直接作为实体字段,属性集合则定义为无 ID 的 Class;运行时以"属性嵌入"或"序列化大对象"两种方式嵌入实体;数据库上通常嵌入实体表,不单独建表。
两者的关系:实体管"身份",值对象管"描述";实体可以独立存在,值对象必须依附于实体。同一个对象在不同业务场景下可能是实体也可能是值对象——比如"地址"在订单场景中是值对象(一次性、整体替换),在行政区划维护中就是实体(有 ID、独立编辑)。判断的依据不是对象本身,而是它在当前限界上下文中扮演什么角色。
值对象的引入还体现了 DDD 的根本主张:优先领域建模,再做数据建模——传统的"一表对应一实体"思路容易让领域模型被数据模型绑架,值对象让两者解耦,使领域建模能更准确地表达业务概念。
先把"是什么"回答清楚
| 概念 | 官方定义(DDD 标准说法) | 一句话大白话 | 例子 |
|---|---|---|---|
| 实体(Entity) | 拥有唯一标识符、且标识在状态变更后仍能保持一致的领域对象;重要的不是属性,而是延续性和标识 | 有 ID 的对象,看 ID 识别身份 | 用户、商品、订单、保单 |
| 值对象(Value Object) | 通过对象属性值识别、将多个相关属性组合为概念整体的对象,无标识符、不可变 | 没 ID 的属性集,看内容识别 | 地址、金额、时间段、姓名 |
再讲"为什么要区分实体和值对象?"
| 解决的问题 | 怎么解决 |
|---|---|
| 实体属性零碎,业务概念被打散 | 把相关属性打包成值对象,保留概念完整性 |
| 数据库表数量多、JOIN 复杂、性能差 | 值对象嵌入实体表,减少表数量 |
| 数据模型绑架业务模型 | 领域建模优先,数据建模服从业务 |
| 同一个对象在不同场景该建成什么? | 看它在当前限界上下文里的角色:要 ID + 独立修改 → 实体;只读 + 整体替换 → 值对象 |
一句话速记
实体看 ID,值对象看内容;实体能改属性,值对象只能整体换。
怎么判断该用实体还是值对象?
按三个问题依次判断:
- 它需要被唯一标识吗?(业务是否在乎它的"身份")
- 是 → 实体
- 否 → 值对象
- 它的属性会被单独频繁修改吗?
- 是 → 实体(属性可变)
- 否 → 值对象(整体替换)
- 它需要被独立查询吗?(按它的某个属性搜索)
- 是 → 实体(独立表、可索引)
- 否 → 值对象(嵌入实体表足够)
典型识别:
- 典型实体:用户、订单、商品、保单、合同、文章
- 典型值对象:地址(省 + 市 + 县 + 街道)、金额(数额 + 货币)、时间段(开始 + 结束)、姓名(姓 + 名)、电话号码(区号 + 号码)
- 容易被忽略的值对象潜力股:审计信息(创建人 + 创建时间 + 修改人 + 修改时间)、坐标(经度 + 纬度)、版本号(主 + 次 + 修订号)
思考题
回到自己的项目:
- 哪些对象明显是实体?(业务在乎它的"身份"、有唯一 ID)
- 哪些是隐藏在实体里的"值对象潜力股"?比如散落在表里的"创建时间 + 修改时间 + 创建人 + 修改人"四个字段,是否可以打包成一个"审计信息"值对象?
- 同一个对象,在不同的限界上下文里,会不会一边是实体、一边是值对象?

