| name | 实体 (Entity) |
| description | 在DDD中具有唯一身份标识和生命周期的对象,通过身份而非属性值相等判断。 |
实体 (Entity)
概述
Eric Evans 在蓝皮书中将实体定义为:拥有贯穿时间与不同表现形式的、独特身份的对象("Objects that have a distinct identity that runs through time and different representations")。两个实体即使属性完全相同,只要身份标识不同,它们就是不同的对象。
核心特征:
- 唯一身份:每个实体有唯一的 ID(标识符),由身份而非属性决定相等
- 生命周期连续性:实体在时间中演化,可历经多种状态与表现形式(数据库行、内存对象、序列化结果)仍是同一个对象
- 可变性:实体在生命周期内可以改变属性(但身份不变)
- 行为承载:实体不只是数据,更应封装业务规则与状态转换
对比值对象:
- 实体:有身份,关心身份相等
- 值对象:无身份,关心值相等
何时使用Entity
应该是Entity的:
- 用户(User)- 每个用户有唯一 ID
- 订单(Order)- 每个订单有唯一订单号
- 产品(Product)- 每个产品有唯一SKU
- 账户(Account)- 每个账户有唯一账号
不应该是Entity的:
- 金额(Money)- 只关心数值,不关心"哪个"金额
- 地址(Address)- 两个相同地址就是一样的
- 颜色(Color)- 只关心颜色值,不关心"哪个"颜色
实体的关键特征
1. 身份标识
public class User {
private final String userId;
private String name;
private String email;
public User(String userId, String name, String email) {
this.userId = userId;
this.name = name;
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(userId, user.userId);
}
@Override
public int hashCode() {
return Objects.hash(userId);
}
}
User user1 = new User("U001", "John", "john@example.com");
User user2 = new User("U002", "John", "john@example.com");
assertNotEquals(user1, user2);
2. 生命周期管理
public class Order {
private final String orderId;
private OrderStatus status;
private List<OrderLine> items;
private double totalAmount;
private Instant createdAt;
private Instant modifiedAt;
public static Order create(String customerId, List<OrderLine> items) {
Order order = new Order(UUID.randomUUID().toString(), items);
order.status = OrderStatus.PENDING;
order.createdAt = Instant.now();
return order;
}
public void pay() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Cannot pay order in status: " + status);
}
this.status = OrderStatus.PAID;
this.modifiedAt = Instant.now();
}
public void ship() {
if (status != OrderStatus.PAID) {
throw new IllegalStateException("Cannot ship unpaid order");
}
this.status = OrderStatus.SHIPPED;
this.modifiedAt = Instant.now();
}
{
(status != OrderStatus.SHIPPED) {
();
}
.status = OrderStatus.DELIVERED;
.modifiedAt = Instant.now();
}
}
Order.create(, items);
order.pay();
order.ship();
order.deliver();
3. 可变性
public class User {
private final String userId;
private String name;
private String email;
public void updateProfile(String newName, String newEmail) {
this.name = newName;
this.email = newEmail;
}
}
User user = new User("U001", "John", "john@example.com");
user.updateProfile("Jane", "jane@example.com");
assertEquals("U001", user.getUserId());
最佳实践
1️⃣ 使用合适的标识符
public class User {
private long id;
}
public class User {
private String userId = UUID.randomUUID().toString();
}
public class Product {
private String sku;
}
2️⃣ 保护实体的一致性
public class BankAccount {
private final String accountId;
private double balance;
public void setBalance(double balance) {
this.balance = balance;
}
public void withdraw(double amount) throws InsufficientFundsException {
if (balance < amount) {
throw new InsufficientFundsException();
}
this.balance -= amount;
}
public void deposit(double amount) throws InvalidAmountException {
if (amount <= 0) {
throw new InvalidAmountException();
}
this.balance += amount;
}
}
3️⃣ 用值对象封装属性
public class User {
private String firstName;
private String lastName;
private String emailValue;
private String emailDomain;
}
public class User {
private final String userId;
private FullName name;
private Email email;
private Address address;
}
4️⃣ 明确边界和聚合
public class Order {
private final String orderId;
private Customer customer;
private List<OrderLine> items;
public void addItem(Product product, int quantity) {
OrderLine line = new OrderLine(product, quantity);
this.items.add(line);
}
public void removeItem(String productId) {
items.removeIf(line -> line.getProduct().getId().equals(productId));
}
}
Java 实现示例
public class Order {
private final String orderId;
private String customerId;
private OrderStatus status;
private List<OrderLine> items;
private Money totalAmount;
private Instant createdAt;
private Instant modifiedAt;
public Order(String orderId, String customerId) {
this.orderId = orderId;
this.customerId = customerId;
this.status = OrderStatus.PENDING;
this.items = new ArrayList<>();
this.createdAt = Instant.now();
}
public void addItem(Product product, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must > 0");
}
OrderLine line = new OrderLine(product, quantity);
items.add(line);
recalculateTotal();
}
public void pay() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Can only pay pending orders");
}
.status = OrderStatus.PAID;
.modifiedAt = Instant.now();
}
{
totalAmount = items.stream()
.map(OrderLine::getTotal)
.reduce(Money.ZERO, Money::add);
}
{
( == o) ;
(!(o Order)) ;
(Order) o;
Objects.equals(orderId, order.orderId);
}
{
Objects.hash(orderId);
}
}
Python 实现示例
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
from typing import List
class OrderStatus(Enum):
PENDING = "PENDING"
PAID = "PAID"
SHIPPED = "SHIPPED"
DELIVERED = "DELIVERED"
@dataclass
class OrderLine:
product_id: str
quantity: int
unit_price: float
def get_total(self) -> float:
return self.quantity * self.unit_price
class Order:
def __init__(self, order_id: str, customer_id: str):
self.order_id = order_id
self.customer_id = customer_id
self.status = OrderStatus.PENDING
self.items: List[OrderLine] = []
self.total_amount = 0.0
self.created_at = datetime.now()
self.modified_at = None
def ():
quantity <= :
ValueError()
.items.append(OrderLine(product_id, quantity, unit_price))
._recalculate_total()
():
.status != OrderStatus.PENDING:
ValueError()
.status = OrderStatus.PAID
.modified_at = datetime.now()
():
.total_amount = (item.get_total() item .items)
():
(other, Order):
.order_id == other.order_id
():
(.order_id)
实体的四种"血液模型"
在实际工程中,实体的代码形态可以按携带业务逻辑的多少分为四种,选型直接影响领域层的厚度与可维护性。
术语对照:英文 DDD 圈通常只区分两种——Anemic Domain Model(贫血领域模型,Fowler 视为反模式) 与 Rich Domain Model(充血模型)。下表"四种血液模型"是中文工程社区的进一步细化划分;阅读 Evans / Vernon 原著时,请把"贫血 / 失血"都对应到 Fowler 的 Anemic 概念,不要混淆。
| 形态 | 包含内容 | 评价 |
|---|
| 失血模型 | 仅有数据字段和 getter/setter(Java 中的 POJO) | 不算领域对象,只是数据容器 |
| 贫血模型 | 数据 + 不依赖持久层的业务逻辑;依赖持久层的业务逻辑放到领域服务中 | 工程推荐,平衡可维护性与充血度 |
| 充血模型 | 数据 + 所有业务逻辑(含依赖持久层的) | 领域对象最"纯",但与持久层耦合 |
| 胀血模型 | 数据 + 业务逻辑 + 与业务无关的其他逻辑(事务、授权、日志等) | 应避免,违反单一职责 |
对比示例
public class Order {
private Long id;
private Integer status;
private BigDecimal amount;
}
public class OrderService {
public void pay(Long orderId) {
Order o = orderDao.get(orderId);
if (o.getStatus() != 0) throw new RuntimeException("状态错误");
o.setStatus(1);
orderDao.update(o);
}
}
public class Order {
private OrderId id;
private OrderStatus status;
private Money amount;
public void pay() {
if (status != OrderStatus.PENDING) {
throw new DomainException("只有待支付订单可以支付");
}
this.status = OrderStatus.PAID;
}
}
public class OrderAppService {
public void pay {
orderRepo.findById(id);
order.pay();
orderRepo.save(order);
}
}
{
{
(status != OrderStatus.PENDING) { ... }
.status = OrderStatus.PAID;
OrderRepository.save();
}
}
{
{
AuditLog.log();
}
}
经验建议:
- 首选贫血模型:实体带业务规则,但不依赖仓储;仓储由应用服务或领域服务调用
- 实体 + 领域服务 = 完整领域模型
- 避免胀血模型:事务、权限、日志等横切关注点属于应用层或 AOP,不属于领域层
与值对象的对比
| 特性 | Entity | Value Object |
|---|
| 身份 | 有唯一 ID | 无 ID |
| 相等性 | 基于 ID | 基于属性值 |
| 可变性 | 通常可变 | 不可变 |
| 生命周期 | 有明确生命周期 | 无生命周期 |
| 例子 | User, Order | Money, Email, Address |
实体 vs DTO
实体经常被误用为 DTO(数据传输对象),导致领域层退化为"带方法的字段袋"。二者必须严格区分:
| 维度 | Entity(实体) | DTO(数据传输对象) |
|---|
| 所属层 | 领域层 | 接口层 / 应用层 |
| 目的 | 承载业务规则与状态转换 | 在层与层、进程与进程之间搬运数据 |
| 行为 | 有业务方法(pay、ship、approve) | 通常只有字段 + getter/setter |
| 不变量 | 自我保护、构造即合法 | 不维护不变量,由发送方负责 |
| 身份 | 有领域标识(OrderId) | 通常无标识 |
| 序列化 | 不直接序列化(避免暴露内部结构) | 为序列化而生(JSON、ProtoBuf) |
| 生命周期 | 与业务对象一致,长久存在 | 一次请求/响应即弃 |
判别关键:如果一个类既要被序列化为 HTTP 响应,又要承载业务规则,几乎可以肯定是把 Entity 和 DTO 揉在了一起——拆开。
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id) {
return orderRepository.findById(id);
}
}
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public OrderDTO getOrder(@PathVariable String id) {
Order order = orderRepository.findById(id);
return OrderDTO.from(order);
}
}
常见误区
❌ 用属性判等——equals 比对 name + email,导致两个不同 userId 的 User 因属性相同被认为相等
→ 实体的相等性永远基于身份标识,与任何属性无关
❌ 全字段 setter 暴露——实体退化为数据袋,业务规则散落到 Service
→ 暴露业务方法(pay、ship、cancel),让状态变化必经规则校验
❌ 持久化可计算字段——把 totalAmount 字段化并要求外部维护一致性
→ 能从其他字段算出的不必字段化;必须字段化时由聚合根内部计算与维护
总结
Entity 的核心:
- 唯一身份标识
- 通过身份判断相等
- 有明确的生命周期
- 可变但需保护一致性
最佳实践:
- 选择合适的标识符(UUID、业务键)
- 通过 ID 而非属性判断相等
- 用值对象组织属性
- 保护实体的不变式
Entity 是 DDD 的核心概念,正确使用能大幅提高设计质量。