Skip to content

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 用得越多越好(线程安全、可缓存、容易测试)。

关键属性 ​

属性EntityValue 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 示例) ​

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 设计 ​

java
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
}

常见误区 ​

  1. "所有领域对象都是 Entity"——错。VO 应该占 70%。能用 VO 就用 VO,不可变 = 安全 + 性能
  2. "VO 就是 DTO"——错。VO 是领域对象(在领域层),DTO 是传输对象(在接口层)。VO 有业务行为,DTO 只是数据袋
  3. "Money 直接用 BigDecimal"——错。BigDecimal 没有币种,1 USD + 1 CNY = 2 还是错误?用 Money(amount, currency) 让业务错误在编译期就发生
  4. "Entity 用==判等"——错。Java 默认 == 是引用相等,两个 Order 内容一样但实例不同 → 不等。必须重写 equals 用 ID 判等
  5. "VO 加 setter 变成可改"——错。一旦加了 setter 就不是 VO 了。改 = 整个换新实例

关键洞察 ​

Entity 解决"身份"问题,VO 解决"特征"问题——业务上 90% 的对象都是特征。

一旦养成"能 VO 就 VO"的习惯,系统的线程安全、缓存友好性、 测试简洁度全部质变。V2 B4 抽象原则在战术层的最大胜利 = 把业务概念里"没有独立身份"的对象都识别成 VO。

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。