| name | 聚合 (Aggregate) |
| description | 以聚合根为边界,包含多个相关Entity和ValueObject的集合。保证数据一致性和事务边界。 |
聚合 (Aggregate)
概述
Eric Evans 将聚合定义为:一组相关对象的集合,作为数据变更的整体单元("a cluster of associated objects that we treat as a unit for the purpose of data changes")。每个聚合都有一个聚合根——作为外部访问聚合内部对象的唯一入口,以及一条由根的标识划定的一致性边界。
核心概念:
- 聚合根(Aggregate Root):一个 Entity,作为聚合的唯一入口;外部引用只能指向它
- 一致性边界:聚合内的不变量在事务内必须成立
- 事务单位:原则上一个事务只修改一个聚合实例——这是 DDD 的基线规则,目的是保证聚合的独立性、可扩展性与最终一致的演进空间。仅当同一限界上下文内、跨聚合的不变量在业务上确实必须强一致(且最终一致代价高于事务耦合的代价)时,可有保留地放宽;放宽前应优先重新审视聚合边界是否划错了
- 聚合间引用:通过 ID 引用,不通过对象引用
Java 实现
public class Order {
private final String orderId;
private List<OrderLine> items;
private OrderStatus status;
private Money totalAmount;
public void addItem(Product product, int quantity) {
OrderLine line = new OrderLine(product, quantity);
items.add(line);
recalculateTotal();
}
public void cancel() {
if (!canBeCanceled()) {
throw new IllegalStateException("Cannot cancel order");
}
this.status = OrderStatus.CANCELED;
}
private void recalculateTotal() {
totalAmount = items.stream()
.map(OrderLine::getTotal)
.reduce(Money.ZERO, Money::add);
}
private boolean canBeCanceled() {
return status == OrderStatus.PENDING || status == OrderStatus.PAID;
}
}
public class OrderLine {
private final Product product;
private final int quantity;
public Money getTotal() {
return product.getPrice().multiply(quantity);
}
}
Python 实现
class Order:
def __init__(self, order_id: str, customer_id: str):
self.order_id = order_id
self.customer_id = customer_id
self.items = []
self.status = "PENDING"
self.total_amount = Money(0, "USD")
def add_item(self, product: 'Product', quantity: int):
"""通过聚合根添加项"""
line = OrderLine(product, quantity)
self.items.append(line)
self._recalculate_total()
def can_be_canceled(self) -> bool:
return self.status in ["PENDING", "PAID"]
def cancel(self):
if not self.can_be_canceled():
raise ValueError("Cannot cancel order")
self.status = "CANCELED"
def _recalculate_total(self):
self.total_amount = sum(
(line.get_total() for line in self.items),
Money(0, "USD")
)
class OrderLine:
def __init__(self, product: 'Product', quantity: int):
self.product = product
self.quantity = quantity
def get_total(self) -> 'Money':
return self.product.price.multiply(self.quantity)
聚合的边界设计
原则
- 根引用聚合内部:聚合根可以引用内部对象
- 外部引用聚合根:外部只能引用聚合根
- 内部对象不引用外部:不创建循环引用
反例
Order order = orderRepository.find("O001");
OrderLine line = order.getItems().get(0);
line.setQuantity(100);
Order order = orderRepository.find("O001");
order.updateItem(0, 100);
最佳实践
- 设定清晰的聚合边界
- 只通过聚合根操作
- 保护聚合内的一致性
- 聚合根是仓储的访问点
Vaughn Vernon 的四条聚合设计规则
Vaughn Vernon 在 Implementing Domain-Driven Design(IDDD,红皮书)中提炼了四条被广泛认可的聚合设计经验:
- 在聚合边界内保护真正的业务不变量 ——只有"必须强一致"的规则,才值得放进同一个聚合。
- 设计小聚合 ——优先单根 + 少量值对象;避免把所有相关 Entity 全塞进一个根。
- 聚合之间通过全局唯一标识引用 ——不要用对象引用穿透聚合边界。
- 聚合外部使用最终一致性 ——跨聚合的协同通过领域事件与最终一致机制完成。
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderLine> items;
private OrderStatus status;
}
public class Order {
private Customer customer;
private List<OrderLine> items;
public void changeCustomerEmail(String email) {
customer.setEmail(email);
}
}
聚合大小如何把握
"小聚合"是原则,但"多小才算小"没有标准答案。下列实操判据可帮助判断:
判断聚合是否过大:
- 一次加载需要拉取大量子对象(数十乃至数百条),多数操作只用到其中一小部分
- 频繁出现"我只想改这一个字段,却要先 load 整个聚合"的吐槽
- 高并发下因聚合粒度大而触发版本冲突 / 乐观锁失败
- 同一聚合内的不同部分修改频率明显不同——通常意味着隐藏着另一个聚合
判断聚合是否过小(被切碎了):
- 一个业务操作需要同时改 3 个以上聚合并维护强一致
- 频繁需要跨聚合 join 才能完成基本查询
- 业务规则被迫散落到应用服务里"做编排式校验"
经验判据:
- 优先把真正共享一致性的最小子集放进同一聚合
- 当聚合内某子集修改频率独立、查询路径独立,就考虑独立成新聚合
- "先粗后细"——不确定时先做大聚合,等出现性能或冲突信号再拆,比反过来从碎片合并容易得多
常见误区
❌ 上帝聚合——把 Order + Customer + Product + Inventory 全塞进一个根
→ 用 ID 引用其他聚合,每个聚合只承载自己的真实不变量
❌ 从外部直接操作聚合内部对象——order.getItems().add(...) 跳过聚合根校验
→ 暴露行为方法 order.addItem(...),集合修改入口只在聚合根内部
❌ 跨聚合写在一个事务里——一次事务同时更新 Order、Inventory、Payment
→ 同一事务只改一个聚合,跨聚合协同用领域事件 + 最终一致性
聚合是建立清晰边界、保证数据一致的关键设计。