Entity & Value Object(实体与值对象)
解决什么问题:把领域对象分成两类——"有身份会变化的"和"只描述特征不可变的"—— 不区分 = 性能差(该不可变的对象被到处复制传引用)、并发冲突(同一对象多处修改)、 模型失真(业务上的"地址"当 Entity 处理 = 改了原地址、所有引用方都变)。
它是什么
DDD 战术层把领域对象分成两种根本不同的类型:
Entity(实体)——有唯一身份标识、生命周期内属性可变的对象。
- 判定标准:两个实例除了 ID 不同其它都相同 → 仍是不同实体
- 例:Order(订单号 1001 和 1002 内容一样但不是同一个订单)、Customer、Product
Value Object(值对象)——无身份标识、不可变、用值相等性判同的对象。
- 判定标准:属性相同 = 相同对象
- 例:Money(amount=100, currency=CNY)、Address(street=..., city=...)、DateRange
Vernon Ch 5: "An object that is not fundamentally defined by its attributes, but rather by a thread of continuity and identity, is called an Entity."
Vernon Ch 6: "A Value Object is an object that describes some characteristic or attribute but carries no concept of identity."
V2 视角(V2.7 B4 段 4 "抽象的最小单位"):Entity 和 VO 是 OOP 抽象的两端——Entity 抽象"持续变化的同一性",VO 抽象"可复用的特征描述"。VO 用得越多越好(线程安全、可缓存、容易测试)。
关键属性
| 属性 | Entity | Value Object |
|---|---|---|
| 身份 | 有 ID(如 OrderId) | 无 ID |
| 相等性 | 用 ID 判等 | 用值判等 |
| 可变性 | 可变(属性随生命周期变) | 不可变(创建后不能改) |
| 生命周期 | 有创建/销毁 | 无生命周期 |
| 共享 | 不能共享引用 | 可安全共享 |
| 线程安全 | 不安全(要锁) | 天然安全 |
| 持久化 | 有独立表 / 主键 | 嵌入 Entity / JSON 序列化 |
| 判错标志 | 业务说"A 和 A 一样"→ Entity;说"100 元 = 100 元"→ VO |
怎么用
场景 1:从业务里识别 Entity vs VO
业务说:"客户的地址变了"——
- 地址 是 Entity 还是 VO?
- 业务上:地址变了 = 新地址替换旧地址,不是"同一个地址的邮编改了"
- 结论:Address 是 VO
业务说:"订单改了配送地址"——
- 订单 是 Entity 还是 VO?
- 业务上:订单号不变就是同一个订单,但属性可以改
- 结论:Order 是 Entity
V2 B6 段 4 折衷:边界 case(如"同一个地址但换人了")→ 看业务怎么说话。如果业务说"地址变"是日常 → VO;如果业务说"地址变"是异常事件 → Entity。
场景 2:VO 的不可变设计(Java 示例)
// VO 必须不可变——所有字段 final,没有 setter
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount.signum() < 0)
throw new IllegalArgumentException("Money < 0");
this.amount = amount;
this.currency = currency;
}
// 行为返回新对象,不修改自己
public Money add(Money other) {
if (!this.currency.equals(other.currency))
throw new CurrencyMismatchException();
return new Money(this.amount.add(other.amount), this.currency);
}
// equals/hashCode 用值
@Override
public boolean equals(Object o) {
if (!(o instanceof Money)) return false;
Money other = (Money) o;
return this.amount.equals(other.amount)
&& this.currency.equals(other.currency);
}
}场景 3:Entity 的 ID 设计
public class Order { // Entity
private final OrderId id; // ID 永不改变
private OrderStatus status; // 属性可变
private List<OrderItem> items;
// 业务行为改属性,不暴露 setter
public void markPaid(Payment p) {
if (status != OrderStatus.PLACED)
throw new InvalidOrderStateException();
if (!p.amount().equals(total()))
throw new PaymentAmountMismatchException();
this.status = OrderStatus.PAID;
}
}
// ID 用专门类型,避免原始类型满天飞
public record OrderId(UUID value) {
public OrderId { if (value == null) throw ...; }
public static OrderId generate() { return new OrderId(UUID.randomUUID()); }
}场景 4:VO 的工程优势
V2 B3 段 5 "团队可拥有" 视角:VO 是"团队可拥有"的最爱——
- 不可变 → 不需要测试 "线程安全"(节省 30% 测试代码)
- 可序列化 → 天然适合消息传递(事件 / 命令 / 缓存值都是 VO)
- 可缓存 → Money(100, CNY) 全局一份就够
- 可测试 → 不需要 mock
事件 OrderPlaced { ← 整个事件是 VO(不可变 + 值判等)
orderId: OrderId ← Entity 的 ID(跨聚合引用)
items: List<ItemVO> ← 列表里全是 VO
total: Money ← 金额是 VO(无币种错误)
placedAt: Instant ← 时间是 VO
}常见误区
- "所有领域对象都是 Entity"——错。VO 应该占 70%。能用 VO 就用 VO,不可变 = 安全 + 性能
- "VO 就是 DTO"——错。VO 是领域对象(在领域层),DTO 是传输对象(在接口层)。VO 有业务行为,DTO 只是数据袋
- "Money 直接用 BigDecimal"——错。BigDecimal 没有币种,1 USD + 1 CNY = 2 还是错误?用 Money(amount, currency) 让业务错误在编译期就发生
- "Entity 用==判等"——错。Java 默认 == 是引用相等,两个 Order 内容一样但实例不同 → 不等。必须重写 equals 用 ID 判等
- "VO 加 setter 变成可改"——错。一旦加了 setter 就不是 VO 了。改 = 整个换新实例
关键洞察
Entity 解决"身份"问题,VO 解决"特征"问题——业务上 90% 的对象都是特征。
一旦养成"能 VO 就 VO"的习惯,系统的线程安全、缓存友好性、 测试简洁度全部质变。V2 B4 抽象原则在战术层的最大胜利 = 把业务概念里"没有独立身份"的对象都识别成 VO。
相关链接
- Aggregate——Entity 和 VO 在聚合里的角色分工
- Domain Service & Event——VO 在事件 / 服务中的复用
- Ubiquitous Language——Entity / VO 的命名从语言来
- [V3 涌现作为] · 抽象——VO 是抽象原则的最小体现
- [V2 B6 折衷] · 折衷——Entity vs VO 的边界判定
- [V3 涌现作为] · 分解——VO 让分解更彻底