en un clic
en un clic
以聚合根为边界,包含多个相关Entity和ValueObject的集合。保证数据一致性和事务边界。
封装复杂对象和聚合的创建过程,将创建职责从领域对象中剥离,保证聚合创建时的不变量满足。
没有身份标识,通过属性值判断相等的对象。不可变,通常代表领域中的度量或描述。
命令查询责任分离,将数据的写入操作和读取操作分别用不同的模型处理,优化各自的性能。
将DDD战略设计应用于微服务架构,限界上下文指导服务拆分,领域事件实现服务间通信。
对比 MVC 模式与 DDD 方法论的差异,分析 MVC 在复杂业务下的局限性,帮助团队判断何时应采用 DDD。
| name | 实体 (Entity) |
| description | 在DDD中具有唯一身份标识和生命周期的对象,通过身份而非属性值相等判断。 |
Eric Evans 在蓝皮书中将实体定义为:拥有贯穿时间与不同表现形式的、独特身份的对象("Objects that have a distinct identity that runs through time and different representations")。两个实体即使属性完全相同,只要身份标识不同,它们就是不同的对象。
核心特征:
对比值对象:
应该是Entity的:
不应该是Entity的:
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 不同
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
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());
// ❌ 使用自增数字(可能重用)
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; // 库存保留单位,业务上唯一
}
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;
}
}
// ❌ 混乱的属性
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; // 值对象:封装地址
}
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));
}
}
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);
}
}
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) | 不算领域对象,只是数据容器 |
| 贫血模型 | 数据 + 不依赖持久层的业务逻辑;依赖持久层的业务逻辑放到领域服务中 | 工程推荐,平衡可维护性与充血度 |
| 充血模型 | 数据 + 所有业务逻辑(含依赖持久层的) | 领域对象最"纯",但与持久层耦合 |
| 胀血模型 | 数据 + 业务逻辑 + 与业务无关的其他逻辑(事务、授权、日志等) | 应避免,违反单一职责 |
// ❌ 失血模型:只有字段,业务逻辑全部外泄
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("..."); // ← 和业务无关的东西
// ...
}
}
经验建议:
| 特性 | Entity | Value Object |
|---|---|---|
| 身份 | 有唯一 ID | 无 ID |
| 相等性 | 基于 ID | 基于属性值 |
| 可变性 | 通常可变 | 不可变 |
| 生命周期 | 有明确生命周期 | 无生命周期 |
| 例子 | User, Order | Money, Email, Address |
实体经常被误用为 DTO(数据传输对象),导致领域层退化为"带方法的字段袋"。二者必须严格区分:
| 维度 | Entity(实体) | DTO(数据传输对象) |
|---|---|---|
| 所属层 | 领域层 | 接口层 / 应用层 |
| 目的 | 承载业务规则与状态转换 | 在层与层、进程与进程之间搬运数据 |
| 行为 | 有业务方法(pay、ship、approve) | 通常只有字段 + getter/setter |
| 不变量 | 自我保护、构造即合法 | 不维护不变量,由发送方负责 |
| 身份 | 有领域标识(OrderId) | 通常无标识 |
| 序列化 | 不直接序列化(避免暴露内部结构) | 为序列化而生(JSON、ProtoBuf) |
| 生命周期 | 与业务对象一致,长久存在 | 一次请求/响应即弃 |
判别关键:如果一个类既要被序列化为 HTTP 响应,又要承载业务规则,几乎可以肯定是把 Entity 和 DTO 揉在了一起——拆开。
// ❌ 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 的核心:
最佳实践:
Entity 是 DDD 的核心概念,正确使用能大幅提高设计质量。