在领域驱动设计(DDD,Domain-Driven Design)中,实体(Entity)和值对象(Value Object)是两种核心的领域模型概念,它们用于表示业务中的对象。
在领域驱动设计(DDD)中,实体(Entity)和值对象(Value Object)是两种核心的领域模型类型,用于表示领域中的业务概念和数据。理解实体与值对象的区别和使用场景是掌握领域建模的关键。

本文将帮助理解两者的区别以及如何在生产开发中正确区分和定义。
1. 实体(Entity)
定义
实体是领域中的一个业务概念,它具有唯一标识(ID),这个标识用来区分同类型的不同实体。
即使实体的其他属性值发生变化,只要它的唯一标识(ID)保持不变,它仍然是同一个实体。
特点
-
唯一标识(ID):
实体必须具有唯一标识(例如主键 ID),通过唯一标识来确认其身份。 -
可变性:
实体的属性值可以随时间变化。例如,用户的地址或订单状态可能会更新。 -
生命周期:
实体具有明确的生命周期,从创建到销毁可能经历多个状态变化。 -
业务行为:
实体通常是业务逻辑的主要载体,因为它们承载了核心的业务操作。
实体的关键特性:
- 通过唯一标识判断同一性。
- 是业务中的“独立角色”。
- 属性的变化不影响其身份。
示例
在电商系统中,一个订单(Order)就是一个实体:
- 它有唯一的订单 ID 来标识它。
- 订单的状态(如“已创建”、“已发货”)可能会随时间变化。
示例代码:
public class Order {
private final String orderId; // 唯一标识
private String status; // 订单状态
private double totalAmount; // 总金额
public Order(String orderId, String status, double totalAmount) {
this.orderId = orderId;
this.status = status;
this.totalAmount = totalAmount;
}
public String getOrderId() {
return orderId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public double getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(double totalAmount) {
this.totalAmount = totalAmount;
}
// 重写 equals 和 hashCode 方法,仅比较 id
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Order order = (Order) o;
return orderId.equals(order.orderId);
}
@Override
public int hashCode() {
return orderId.hashCode();
}
2. 值对象(Value Object)
定义
值对象表示业务中的属性或状态,它没有唯一标识,通过属性值相等来判断两个值对象是否相等。
特点
-
无唯一标识:
值对象没有唯一标识,它通过属性值来判断是否相同(两个值对象如果属性值相同,就认为它们是相等的)。 -
不可变性:
值对象通常是不可变的。一旦创建,其属性值不能再被修改。如果需要修改值对象,应创建一个新的实例(创建一个新的值对象来替代)。 -
用于描述属性:
值对象通常用来表示实体的属性或某些业务概念,帮助实体表达更复杂的业务逻辑,如地址(Address)、货币金额(Money)等。 -
业务逻辑的载体:
值对象也可以包含与其相关的业务逻辑。例如,货币金额对象可以包含加减操作。
值对象的关键特性:
- 通过属性值判断相等性。
- 主要用来描述领域中的“值”。
- 是不可变的(不可变性可以提高代码的安全性和可维护性)。
示例
在电商系统中,一个地址(Address)就是一个值对象:
- 它没有唯一标识,只有地址的属性值(如街道、城市)来判断是否相等。
- 如果地址发生变化,则需要创建一个新的地址对象。
示例代码:
public class Address {
private final String street; // 街道
private final String city; // 城市
private final String zipCode; // 邮政编码
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getZipCode() {
return zipCode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return street.equals(address.street) &&
city.equals(address.city) &&
zipCode.equals(address.zipCode);
}
@Override
public int hashCode() {
return Objects.hash(street, city, zipCode);
}
}
3. 实体与值对象的区别
特性 | 实体(Entity) | 值对象(Value Object) |
---|---|---|
标识 | 有唯一标识,用于区分实例 | 无唯一标识,通过属性值比较是否相等 |
相等性判断 | 基于唯一标识判断相等 | 基于属性值判断相等 |
可变性 | 通常是可变的 | 通常是不可变的 |
用途 | 表示业务中的独立角色 | 表示业务中的属性或状态 |
生命周期 | 生命周期明确,可以存在较长时间 | 生命周期通常由其所属的实体决定,随实体一起存在与销毁 |
业务场景 | 用户、订单、商品等 | 地址、货币金额、坐标等 |
4. 实体与值对象的使用场景
实体的使用场景
用户管理系统: 用户、订单、商品等独立对象。
订单系统: 每个订单具有唯一标识(订单号),即使订单的状态或金额发生变化,它仍是同一个订单。
- 订单(Order):具有唯一标识和生命周期,状态可能变化。
- 用户(User):每个用户都由唯一标识(如用户 ID)标识。
- 产品(Product):每个产品都有唯一的 SKU 或产品编号。
值对象的使用场景
地址管理: 地址通常作为用户的属性,如果地址发生变化,我们只需替换成一个新的值对象。
货币管理: 金额和币种的组合可以作为一个值对象(如 100 USD 和 200 CNY)。
- 地址(Address):表示街道、城市、邮政编码等,无需唯一标识。
- 金额(Money):表示货币金额(如 100 元),只需要通过数值和币种比较。
- 时间范围(DateRange):表示开始时间和结束时间。
5. 为什么区分实体和值对象很重要?
-
建模更清晰:
如果不清楚区分实体和值对象,容易导致模型复杂化,例如为值对象误设唯一标识,或者让实体中充满不必要的状态。 -
简化业务逻辑:
值对象的不可变性可以减少并发修改的问题,同时值对象的属性值比较也更自然。 -
提高性能:
值对象通常可以直接复用,不需要像实体那样额外进行唯一性校验或管理。 -
降低代码复杂度:
通过值对象封装属性,可以避免将所有细节直接暴露在实体中,从而让代码更易于维护。
6. 在生产开发中如何区分和定义实体与值对象
在实际开发中,我们需要根据业务需求来判断一个对象是实体还是值对象。以下是两者的定义和设计准则。
6.1 如何定义实体
判断准则:
-
是否需要唯一标识:
如果业务中需要明确区分某个对象的“身份”,那么它是实体。例如,一个用户的 ID 是其身份的唯一标识,即使姓名或其他信息发生变化,用户的身份不变。 -
是否有独立生命周期:
如果对象的生命周期是独立于其他对象的,它通常是实体。例如,订单可以单独存在,即使用户被删除,订单仍然存在。
6.2 如何定义值对象
判断准则:
-
是否依赖于属性值:
如果对象没有唯一标识,并且其意义完全由属性值决定,那么它是值对象。例如,货币金额(100 USD)是值对象,不需要唯一标识。 -
是否是不可变的:
值对象应当设计为不可变。一旦创建了值对象,其属性值不能再被修改。
6.3 在生产开发中的设计注意事项
-
明确唯一标识的需求:
如果对象需要唯一标识,则设计为实体;如果对象仅根据属性值比较相等,则设计为值对象。 -
保持值对象的不可变性:
在设计值对象时,避免提供修改属性值的方法(如 Setter),确保它是不可变的。 -
避免误用:
不要为了方便,将所有对象都设计成实体或值对象;区分的基础是业务需求。 -
性能优化:
如果值对象过于复杂(如嵌套属性过多),应注意其在内存中的创建和比较成本。
6.4 小结
- 实体: 代表有唯一标识的独立业务对象,用来表示业务中的“谁”。
- 值对象: 用来表示业务中的属性或状态,没有唯一标识,用来表示“什么”。
- 在生产开发中,根据业务需求区分实体与值对象,并严格遵守不可变性(值对象)和唯一标识(实体)的设计原则,从而提升代码的可维护性和业务的表达能力。
7. 实体与值对象的组合使用
在实际场景中,实体和值对象往往是组合使用的。例如:
- 一个订单(实体
Order
)可能包含多个商品明细,每个商品明细是一个值对象。
示例代码:
import java.util.ArrayList;
import java.util.List;
public class Order {
private final String orderId; // 订单唯一标识
private final String userId; // 用户唯一标识
private final List<OrderItem> items; // 订单商品明细
public Order(String orderId, String userId) {
this.orderId = orderId;
this.userId = userId;
this.items = new ArrayList<>();
}
public String getOrderId() {
return orderId;
}
public String getUserId() {
return userId;
}
public List<OrderItem> getItems() {
return items;
}
public void addItem(OrderItem item) {
this.items.add(item);
}
}
/**
* 订单明细(值对象)。
* 表示每个订单中商品的具体信息。
*/
public class OrderItem {
private final String productId; // 商品 ID
private final int quantity; // 数量
private final double price; // 单价
public OrderItem(String productId, int quantity, double price) {
this.productId = productId;
this.quantity = quantity;
this.price = price;
}
public String getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
public double getPrice() {
return price;
}
}
总结
- 实体(Entity)表示领域中的核心业务角色,具有唯一标识,属性和状态可变,生命周期明确。
- 值对象(Value Object)表示领域中的属性或状态,通常不可变,通过属性值判断相等。
理解实体与值对象的区别和正确使用场景,可以帮助我们设计更清晰的领域模型,减少复杂度,增强代码的可维护性和可读性。