在领域驱动设计(DDD,Domain-Driven Design)中,**聚合(Aggregate)**是一个核心概念,用于定义领域模型中一组紧密关联的对象,以及它们之间的边界和关系。
简单来说,聚合是一组具有强一致性约束的领域对象的集合,这些对象是作为一个整体进行操作的。

聚合的定义
聚合是由一个聚合根(Aggregate Root)及其相关对象组成的边界内的集合。
聚合根是聚合的入口点,对聚合内的所有对象负责。
聚合帮助我们定义哪些对象需要一起变化、被一起操作,同时帮助我们控制领域模型的复杂性。
聚合的设计原则
-
聚合定义边界:
聚合将领域对象划分成多个边界,每个边界内包含一个聚合根和其管理的相关对象。聚合外部的代码不能直接操作聚合内的其他对象。 -
聚合根是唯一的入口:
只有通过聚合根,才能访问或操作聚合内的对象。聚合内的其他对象(称为“子实体”或“值对象”)只能通过聚合根来操作。 -
聚合保证一致性:
聚合在某个时间点内应该保持一致性。当修改聚合时,一次操作应确保所有规则和约束都满足。 -
聚合避免过大:
聚合的边界不宜过大,否则会导致性能和复杂性问题。建议一个聚合尽量保持小而清晰。 -
聚合根有唯一标识:
聚合根通常有一个全局唯一的标识符,用于标识整个聚合。
为什么需要聚合
-
复杂性管理:
聚合为领域模型分割出清晰的边界,避免模型变得混乱和难以维护。 -
一致性控制:
聚合内部的对象必须通过聚合根来操作,从而保证聚合内部的状态一致性。 -
简化持久化:
聚合让我们可以将一组相关对象作为整体进行存储、查询和更新。 -
保护领域模型:
聚合外部的代码无法直接访问聚合内的非聚合根对象,从而保护领域模型的完整性。
聚合的组成
-
聚合根(Aggregate Root):
- 聚合的核心对象。
- 负责管理聚合内部的对象。
- 对外提供聚合的唯一入口,外界只能通过聚合根访问聚合内部的对象。
- 有一个全局唯一标识符(ID)。
-
实体(Entity):
- 聚合内的独立对象,具有唯一标识符。
- 聚合内的实体只能通过聚合根访问,不能直接被外部操作。
-
值对象(Value Object):
- 聚合内的无唯一标识的对象,用于描述实体的属性或行为。
- 值对象是不可变的。
聚合的例子
示例场景:订单和订单项
我们以一个电子商务系统为例,“订单(Order)和订单项(OrderItem)”的关系很适合作为聚合的示例。
- 订单(Order)是聚合根,负责管理整个订单。
- 订单项(OrderItem)是订单的组成部分,不能脱离订单单独存在。
- 值对象可以是“商品价格”和“折扣信息”。
示例代码
import java.util.ArrayList;
import java.util.List;
/**
* 值对象:价格。
* 无唯一标识,仅描述商品的属性。
*/
class Price {
private final double amount; // 金额
private final String currency; // 货币单位
public Price(double amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public double getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
}
/**
* 实体:订单项(OrderItem)。
* 订单项是聚合内的子实体,描述订单中的单个商品信息。
*/
class OrderItem {
private final String productId; // 商品 ID
private final int quantity; // 商品数量
private final Price price; // 商品价格
public OrderItem(String productId, int quantity, Price price) {
this.productId = productId;
this.quantity = quantity;
this.price = price;
}
public String getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
public Price getPrice() {
return price;
}
}
/**
* 聚合根:订单(Order)。
* 订单是聚合的根,负责管理订单项。
*/
class Order {
private final String orderId; // 订单唯一标识
private final String userId; // 用户 ID
private final List<OrderItem> items; // 订单中的商品列表
public Order(String orderId, String userId) {
this.orderId = orderId;
this.userId = userId;
this.items = new ArrayList<>();
}
public String getOrderId() {
return orderId;
}
public String getUserId() {
return userId;
}
/**
* 添加订单项。
*
* @param item 要添加的订单项
*/
public void addItem(OrderItem item) {
items.add(item);
}
/**
* 获取所有订单项。
*
* @return 订单项列表
*/
public List<OrderItem> getItems() {
return items;
}
/**
* 计算订单总金额。
*
* @return 订单总金额
*/
public double calculateTotal() {
return items.stream()
.mapToDouble(item -> item.getPrice().getAmount() * item.getQuantity())
.sum();
}
}
聚合的特点
-
外部只能操作聚合根:
在上述代码中,订单项(OrderItem
)不能单独存在,所有操作必须通过订单(Order
)来进行,比如添加订单项、获取订单项列表等。 -
聚合根保证一致性:
聚合根负责维护聚合内的业务规则。例如,订单的总金额是由所有订单项的金额计算得出的,外部无法直接修改订单项,只能通过聚合根进行。 -
聚合的边界清晰:
订单是聚合的边界,订单项和价格等对象只能通过订单访问。
聚合在生产环境中的使用
在实际开发中,聚合通常会与仓储一起使用。仓储负责保存和加载聚合根,同时通过聚合根间接管理聚合内的其他对象。
示例:基于 MyBatis 的聚合操作
public interface OrderMapper {
// 保存订单
void insertOrder(Order order);
// 保存订单项
void insertOrderItem(OrderItem orderItem);
// 根据订单 ID 查询订单
Order selectOrderById(String orderId);
// 根据订单 ID 查询订单项
List<OrderItem> selectOrderItemsByOrderId(String orderId);
}
总结
- 聚合定义了一组具有强一致性约束的对象集合,并通过聚合根管理其内部状态。
- 聚合根是外界与聚合交互的唯一入口。
- 聚合控制了领域模型的边界,避免了直接操作子对象导致的不一致性。
- 生产环境中,聚合与仓储结合使用,既保证模型的清晰性,又便于持久化和扩展。
通过聚合设计,可以更好地管理领域模型的复杂性,保持模型的内聚性和一致性,从而实现健壮的业务逻辑实现。