- 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. 身份标识
```java
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); // 只比对 ID
}
@Override
public int hashCode() {
return Objects.hash(userId); // 只用 ID 计算哈希
}
}
// 两个用户,相同属性但不同ID → 不相等
User user1 = new User("U001", "John", "john@example.com");
User user2 = new User("U002", "John", "john@example.com");
assertNotEquals(user1, user2); // 不相等,因为 ID 不同
```
### 2. 生命周期管理
```java
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();
}
public void deliver() {
if (status != OrderStatus.SHIPPED) {
throw new IllegalStateException("Cannot deliver unshipped order");
}
this.status = OrderStatus.DELIVERED;
this.modifiedAt = Instant.now();
}
}
// 订单的生命周期
Order order = Order.create("C001", items);
order.pay(); // PENDING → PAID
order.ship(); // PAID → SHIPPED
order.deliver(); // SHIPPED → DELIVERED
```
### 3. 可变性
```java
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;
}
}
// 实体的属性可以改变,但 ID 不变
User user = new User("U001", "John", "john@example.com");
user.updateProfile("Jane", "jane@example.com");
// 仍然是同一个用户,只是属性改变了
assertEquals("U001", user.getUserId());
```
## 最佳实践
### 1️⃣ 使用合适的标识符
```java
// ❌ 使用自增数字(可能重用)
public class User {
private long id; // 1, 2, 3... 不够唯一
}
// ✅ 使用 UUID(全局唯一)
public class User {
private String userId = UUID.randomUUID().toString();
}
// ✅ 使用业务标识符(有含义)
public class Product {
private String sku; // 库存保留单位,业务上唯一
}
```
### 2️⃣ 保护实体的一致性
```java
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️⃣ 用值对象封装属性
```java
// ❌ 混乱的属性
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; // 值对象:firstName + lastName
private Email email; // 值对象:封装邮箱逻辑
private Address address; // 值对象:封装地址
}
```
### 4️⃣ 明确边界和聚合
```java
public class Order {
private final String orderId;
private Customer customer; // 引用,不嵌入
private List<OrderLine> items; // 从属(聚合根)
// ❌ 不要直接修改 items
// public List<OrderLine> getItems() { return 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 实现示例
```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");
}
this.status = OrderStatus.PAID;
this.modifiedAt = Instant.now();
}
private void recalculateTotal() {
totalAmount = items.stream()
.map(OrderLine::getTotal)
.reduce(Money.ZERO, Money::add);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order)) return false;
Order order = (Order) o;
return Objects.equals(orderId, order.orderId);
}
@Override
public int hashCode() {
return Objects.hash(orderId);
}
}
```
## Python 实现示例
```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 add_item(self, product_id: str, quantity: int, unit_price: float):
if quantity <= 0:
raise ValueError("Quantity must > 0")
self.items.append(OrderLine(product_id, quantity, unit_price))
self._recalculate_total()
def pay(self):
if self.status != OrderStatus.PENDING:
raise ValueError(f"Cannot pay order in status {self.status}")
self.status = OrderStatus.PAID
self.modified_at = datetime.now()
def _recalculate_total(self):
self.total_amount = sum(item.get_total() for item in self.items)
def __eq__(self, other):
# 通过 ID 判断相等
if not isinstance(other, Order):
return False
return self.order_id == other.order_id
def __hash__(self):
return hash(self.order_id)
```
## 实体的四种"血液模型"
在实际工程中,实体的代码形态可以按**携带业务逻辑的多少**分为四种,选型直接影响领域层的厚度与可维护性。
> **术语对照**:英文 DDD 圈通常只区分两种——**Anemic Domain Model(贫血领域模型,Fowler 视为反模式)** 与 **Rich Domain Model(充血模型)**。下表"四种血液模型"是中文工程社区的进一步细化划分;阅读 Evans / Vernon 原著时,请把"贫血 / 失血"都对应到 Fowler 的 Anemic 概念,不要混淆。
| 形态 | 包含内容 | 评价 |
|------|----------|------|
| **失血模型** | 仅有数据字段和 getter/setter(Java 中的 POJO) | 不算领域对象,只是数据容器 |
| **贫血模型** | 数据 + 不依赖持久层的业务逻辑;**依赖持久层的业务逻辑放到领域服务**中 | **工程推荐**,平衡可维护性与充血度 |
| **充血模型** | 数据 + 所有业务逻辑(含依赖持久层的) | 领域对象最"纯",但与持久层耦合 |
| **胀血模型** | 数据 + 业务逻辑 + 与业务无关的其他逻辑(事务、授权、日志等) | 应避免,违反单一职责 |
### 对比示例
```java
// ❌ 失血模型:只有字段,业务逻辑全部外泄
public class Order {
private Long id;
private Integer status;
private BigDecimal amount;
// 50 行 getter/setter ...
}
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(OrderId id) {
Order order = orderRepo.findById(id);
order.pay(); // 调业务方法
orderRepo.save(order); // 持久化由仓储负责
}
}
// 充血模型:持久化也在实体内(少用)
public class Order {
public void pay() {
if (status != OrderStatus.PENDING) { ... }
this.status = OrderStatus.PAID;
OrderRepository.save(this); // 持久化耦合进实体
}
}
// ❌ 胀血模型:一锅端,权限/事务/日志全塞进来
public class Order {
@Transactional
@RequiresPermission("ORDER_PAY")
public void pay(User operator) {
AuditLog.log("..."); // ← 和业务无关的东西
// ...
}
}
```
**经验建议**:
- **首选贫血模型**:实体带业务规则,但不依赖仓储;仓储由[应用服务](../application-service/)或[领域服务](../domain-service/)调用
- **实体 + 领域服务 = 完整领域模型**
- 避免胀血模型:事务、权限、日志等横切关注点属于应用层或 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 揉在了一起——拆开。
```java
// ❌ Entity 与 DTO 混用
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id) {
return orderRepository.findById(id); // 直接把 Entity 序列化返回
}
}
// ✅ 用 DTO 隔离边界
@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` 字段化并要求外部维护一致性
→ 能从其他字段算出的不必字段化;必须字段化时由聚合根内部计算与维护
Ver no GitHub