你想知道在DDD中如何设计出合理的聚合根,核心是要找准聚合的边界、明确聚合根的职责,并通过聚合根保障业务规则和数据一致性,避免聚合过大或过小、职责混乱等常见问题。
设计合理的聚合根没有绝对的标准答案,但有一套经过实践验证的核心原则和步骤,下面我会从识别原则、设计步骤、实战技巧、避坑指南四个维度,结合具体例子帮你掌握设计方法。
一、核心识别原则(先判断“谁能当”聚合根)
设计聚合根的第一步,是先从领域模型中筛选出符合聚合根特征的实体,核心原则有5个:
-
有独立的生命周期
聚合根可以独立存在,聚合内的其他对象(实体/值对象)必须依赖它。
✅ 正确示例:订单(Order)可独立存在,订单项(OrderItem)不能脱离订单;
❌ 错误示例:把“收货地址”设为聚合根,因为地址依赖于“用户”或“订单”,无独立生命周期。 -
有全局唯一的标识
聚合根的ID是跨系统/跨聚合唯一的(如UUID、自增ID),而聚合内其他实体的ID仅需在聚合内唯一(如订单项的序号)。 -
承担核心业务规则
聚合内的核心业务逻辑(如库存校验、订单状态流转)必须由聚合根封装,而非外部调用者。
✅ 正确示例:Order.AddItem()方法内校验“商品库存≥购买数量”;
❌ 错误示例:外部代码先查库存,再直接给Order.Items加订单项(易导致并发下数据不一致)。 -
是外部访问的唯一入口
外部对象只能引用聚合根,不能直接引用聚合内的其他对象。
✅ 正确示例:通过OrderRepository.GetById(orderId)获取订单,再从订单中取订单项;
❌ 错误示例:提供OrderItemRepository.GetById(itemId)直接查询订单项。 -
能保证聚合内数据一致性
聚合根要确保聚合内所有对象的状态符合业务规则,一次操作要么全部成功,要么全部失败(无需分布式事务,聚合内是内存操作)。
二、落地设计步骤(从0到1设计聚合根)
以“电商下单”场景为例,一步步设计聚合根:
步骤1:梳理业务场景和核心规则
- 场景:用户下单,包含多个订单项,需校验商品库存、计算总价、生成订单编号;
- 核心规则:
① 订单项数量不能为0;
② 商品库存≥购买数量;
③ 订单创建后状态为“待支付”;
④ 订单总价=∑(商品单价×数量)。
步骤2:划分聚合边界(确定聚合包含的对象)
- 聚合根:
Order(订单); - 聚合内对象:
- 实体:
OrderItem(订单项,有自己的ID和属性); - 值对象:
Address(收货地址,无唯一ID,属性不可变)、Money(金额,包含币种和数值)。
- 实体:
步骤3:封装聚合根的行为(核心)
聚合根的核心是“行为”而非“属性”,所有对聚合内对象的修改必须通过聚合根的方法,而非直接修改属性。
// 以Java为例,聚合根:Order
public class Order {
// 聚合根ID(全局唯一)
private final OrderId id;
// 用户ID(关联其他聚合根,仅存ID,避免跨聚合引用)
private final UserId userId;
// 聚合内对象,私有化,外部不可直接修改
private final List<OrderItem> items;
private Address shippingAddress;
private Money totalAmount;
private OrderStatus status;
// 私有化构造函数,外部只能通过工厂方法创建
private Order(OrderId id, UserId userId, Address shippingAddress) {
this.id = id;
this.userId = userId;
this.shippingAddress = shippingAddress;
this.items = new ArrayList<>();
this.status = OrderStatus.PENDING_PAYMENT;
this.totalAmount = Money.zero();
}
// 工厂方法:创建订单(保障创建时的规则)
public static Order create(UserId userId, Address shippingAddress) {
// 校验必填项
if (userId == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (shippingAddress == null) {
throw new IllegalArgumentException("收货地址不能为空");
}
return new Order(new OrderId(UUID.randomUUID().toString()), userId, shippingAddress);
}
// 聚合根方法:添加订单项(封装核心规则)
public void addItem(Product product, int quantity, StockService stockService) {
// 规则1:数量校验
if (quantity <= 0) {
throw new IllegalArgumentException("订单项数量不能小于等于0");
}
// 规则2:库存校验
if (!stockService.hasStock(product.getId(), quantity)) {
throw new IllegalStateException("商品" + product.getName() + "库存不足");
}
// 创建订单项并添加
OrderItem item = new OrderItem(product.getId(), product.getName(), product.getPrice(), quantity);
this.items.add(item);
// 规则4:更新总价
this.totalAmount = this.totalAmount.add(product.getPrice().multiply(quantity));
}
// 仅提供只读的getter,避免外部修改
public OrderId getId() { return id; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
public Money getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
// 其他行为:取消订单、支付订单等(均封装规则)
public void cancel() {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("仅待支付订单可取消");
}
this.status = OrderStatus.CANCELLED;
}
}
// 聚合内值对象:地址
public class Address {
private final String province;
private final String city;
private final String detail;
public Address(String province, String city, String detail) {
// 校验地址合法性
this.province = Objects.requireNonNull(province);
this.city = Objects.requireNonNull(city);
this.detail = Objects.requireNonNull(detail);
}
// 仅getter,无setter(值对象不可变)
public String getProvince() { return province; }
}
步骤4:设计仓储(仅针对聚合根)
仓储是聚合根的持久化入口,只操作聚合根,不操作聚合内的其他对象:
// 订单仓储接口(仅针对聚合根)
public interface OrderRepository {
// 保存整个聚合
void save(Order order);
// 根据ID查询聚合根
Optional<Order> findById(OrderId orderId);
// 其他查询:按用户、按状态等(均返回聚合根)
List<Order> findByUserIdAndStatus(UserId userId, OrderStatus status);
}
// 仓储实现(简化版)
public class JpaOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// 保存订单时,自动级联保存订单项(JPA特性)
entityManager.persist(order);
}
@Override
public Optional<Order> findById(OrderId orderId) {
// 查询订单时,自动级联加载订单项
return Optional.ofNullable(entityManager.find(Order.class, orderId));
}
}
三、实战设计技巧
-
聚合要“小而精”
一个聚合只解决一个核心业务问题,避免“大聚合”(如把“用户+订单+收货地址”都放进一个聚合)。
✅ 推荐:用户聚合(User)、订单聚合(Order)、商品聚合(Product)各自独立;
❌ 避免:Order聚合中包含完整的User对象(只需保存UserId即可)。 -
跨聚合关联用ID,不用对象引用
聚合根之间通过ID关联,而非直接持有对象,降低耦合:
✅ 正确:Order中保存UserId,而非User对象;
❌ 错误:Order中直接持有User对象(会导致两个聚合耦合,且持久化时易出问题)。 -
值对象优先,减少实体数量
聚合内无独立生命周期、无业务行为的对象,优先设计为值对象(如地址、金额、商品规格),值对象不可变,能减少状态管理的复杂度。 -
通过工厂方法创建聚合根
私有化聚合根的构造函数,通过静态工厂方法创建,确保创建时就满足所有业务规则(如必填项、初始状态)。
四、常见避坑指南
| 常见错误 | 问题后果 | 正确做法 |
|---|---|---|
| 聚合根只存属性,无行为 | 业务规则散落在服务层,易导致数据不一致 | 把核心规则封装到聚合根的方法中 |
| 聚合过大(包含过多对象) | 性能差、维护难、并发冲突多 | 拆分小聚合,通过ID关联 |
| 外部直接修改聚合内对象 | 绕过业务规则,数据混乱 | 聚合内对象私有化,仅通过聚合根方法修改 |
| 聚合根之间直接引用对象 | 耦合高,持久化复杂 | 仅保存对方的ID,需要时通过仓储查询 |
| 为聚合内的每个对象设计仓储 | 破坏聚合边界,无法保障一致性 | 仅为聚合根设计仓储 |
总结
设计合理的聚合根,核心要抓住3个关键点:
- 边界清晰:聚合只包含完成一个核心业务功能的对象,聚合要小,跨聚合用ID关联;
- 行为封装:聚合根是业务规则的载体,所有对聚合内对象的修改必须通过聚合根的方法,而非直接操作属性;
- 入口唯一:外部只能通过聚合根访问聚合内的对象,仓储仅针对聚合根设计,保障数据一致性。
简单来说,判断聚合根设计是否合理的终极标准:是否能在不依赖外部事务的情况下,保障聚合内所有业务规则的完整性和数据的一致性。
本文使用 文章同步助手 同步