# 实体 (Entity)

> 在DDD中具有唯一身份标识和生命周期的对象，通过身份而非属性值相等判断。

- Skill: `microwind/entity` (Agent Skill)
- Install (CLI): `npx skillmds@latest add microwind/entity`
- Raw SKILL.md: https://api.skillmd.com/api/skills/microwind/entity/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: microwind (https://skillmd.com/u/microwind)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/microwind/entity

---


# 实体 (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` 字段化并要求外部维护一致性
→ 能从其他字段算出的不必字段化；必须字段化时由聚合根内部计算与维护

---

## 总结

**Entity 的核心**：
- 唯一身份标识
- 通过身份判断相等
- 有明确的生命周期
- 可变但需保护一致性

**最佳实践**：
- 选择合适的标识符（UUID、业务键）
- 通过 ID 而非属性判断相等
- 用值对象组织属性
- 保护实体的不变式

Entity 是 DDD 的核心概念，正确使用能大幅提高设计质量。

