加载中...

本篇要搞懂的 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)

定义(出自《实现领域驱动设计》):通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体,用来描述领域的某个特定方面,没有标识符

值对象有两个关键特征:

  1. 不可变:当度量或描述改变时,用另一个值对象整体替换,而不是修改原对象。
  2. 可做相等性比较:两个值对象的属性值完全相同,就视为相等;并且不会对协作对象产生副作用。

通俗讲:值对象本质上是一个集合——若干用于描述目的、具有整体概念、不可修改的属性。它在领域建模中保证属性归类的清晰和概念的完整性,避免实体属性零碎

🪞 理解辅助:值对象可以类比为"名片上印着的信息"——名字、电话、地址。这些信息没有独立的"地址 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 整体替换
}

关键观察

  • Personid → 实体
  • 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,值对象看内容;实体能改属性,值对象只能整体换。

怎么判断该用实体还是值对象?

按三个问题依次判断:

  1. 它需要被唯一标识吗?(业务是否在乎它的"身份")
    • 是 → 实体
    • 否 → 值对象
  2. 它的属性会被单独频繁修改吗?
    • 是 → 实体(属性可变)
    • 否 → 值对象(整体替换)
  3. 它需要被独立查询吗?(按它的某个属性搜索)
    • 是 → 实体(独立表、可索引)
    • 否 → 值对象(嵌入实体表足够)

典型识别

  • 典型实体:用户、订单、商品、保单、合同、文章
  • 典型值对象:地址(省 + 市 + 县 + 街道)、金额(数额 + 货币)、时间段(开始 + 结束)、姓名(姓 + 名)、电话号码(区号 + 号码)
  • 容易被忽略的值对象潜力股:审计信息(创建人 + 创建时间 + 修改人 + 修改时间)、坐标(经度 + 纬度)、版本号(主 + 次 + 修订号)

思考题

回到自己的项目:

  • 哪些对象明显是实体?(业务在乎它的"身份"、有唯一 ID)
  • 哪些是隐藏在实体里的"值对象潜力股"?比如散落在表里的"创建时间 + 修改时间 + 创建人 + 修改人"四个字段,是否可以打包成一个"审计信息"值对象?
  • 同一个对象,在不同的限界上下文里,会不会一边是实体、一边是值对象?
公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关