在领域驱动设计(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; // 商品 IDprivate 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; // 用户 IDprivate 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);
}
总结
- 聚合定义了一组具有强一致性约束的对象集合,并通过聚合根管理其内部状态。
- 聚合根是外界与聚合交互的唯一入口。
- 聚合控制了领域模型的边界,避免了直接操作子对象导致的不一致性。
- 生产环境中,聚合与仓储结合使用,既保证模型的清晰性,又便于持久化和扩展。
通过聚合设计,可以更好地管理领域模型的复杂性,保持模型的内聚性和一致性,从而实现健壮的业务逻辑实现。