第 5 章:聚合根
学习目标
- 理解聚合根的本质:一致性边界
- 掌握聚合设计的"小而美"原则
- 学会用仓储管理聚合的整个生命周期
- 避开大聚合、跨聚合事务的常见坑
一、聚合的本质
聚合 = 一致性边界,边界内强一致,边界外最终一致。一个聚合 = 一个事务。
text
聚合
├── 聚合根(对外唯一入口)
├── 实体(内部成员)
└── 值对象(内部属性)经典订单聚合:
java
public class Order {
private final OrderId id;
private OrderStatus status;
private final BuyerId buyerId;
private final List<OrderItem> items; // 实体
private final Address shippingAddress; // 值对象
private final Money paidAmount; // 值对象
// 唯一改 items 的入口
public void addItem(SkuId skuId, int quantity, Money unitPrice) {
if (status != OrderStatus.CREATED) {
throw new OrderImmutableException("订单已提交,不能加购");
}
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须 > 0");
}
// 找已有 item?累加:新增
items.stream()
.filter(i -> i.skuId().equals(skuId))
.findFirst()
.ifPresentOrElse(
i -> i.increase(quantity),
() -> items.add(new OrderItem(skuId, quantity, unitPrice))
);
}
public void pay(Money amount) {
if (status != OrderStatus.CREATED) {
throw new OrderImmutableException("订单状态不对:" + status);
}
if (!amount.equals(this.totalAmount())) {
throw new PaymentAmountMismatchException();
}
this.status = OrderStatus.PAID;
this.paidAmount = amount;
}
public Money totalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.zero("CNY"), Money::add);
}
}二、聚合设计的"小而美"
聚合越小越好。一个聚合应该能在几毫秒内完成加载和保存。
java
// ✅ 小聚合:订单本身
public class Order {
private List<OrderItem> items; // 只存必要的
}
// ❌ 大聚合:订单 + 收货地址簿 + 优惠券 + 推荐列表
public class BigOrder {
private List<OrderItem> items;
private AddressBook addressBook; // 不必每次都加载
private List<Coupon> coupons; // 单独的优惠券聚合
private List<Product> recommendations; // 推荐系统独立
}⚠️ 坑 1:聚合太大,每次只改一个字段也要加载全部数据,加锁时间长,高并发下锁竞争严重。
判断标准:这些对象是否必须一起修改、一起提交?
java
// 订单 + 订单项 → 一起改 → 同一聚合
// 订单 + 库存 → 各自独立 → 不同聚合,异步协调
// 订单 + 用户 → 各自独立 → 不同聚合,用 ID 引用三、聚合内不变量(Invariant)
聚合根守护一组业务不变量:
java
public class Order {
private final List<OrderItem> items;
// 不变量 1:订单至少 1 个商品
public void submit() {
if (items.isEmpty()) {
throw new EmptyOrderException();
}
this.status = OrderStatus.SUBMITTED;
}
// 不变量 2:商品总价 = 各项之和
public Money totalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.zero("CNY"), Money::add);
}
// 不变量 3:未支付订单 30 分钟过期
public boolean isExpired(LocalDateTime now) {
return createdAt.plus(30, ChronoUnit.MINUTES).isBefore(now);
}
}外部任何修改都要走聚合根的方法,绕开方法直接改字段 = 破坏不变量。
⚠️ 坑 2:
items没有private final或者暴露getItems()返回可变 List,外部可以add/remove绕过校验。严格封装。
四、聚合根 vs 仓储
仓储只针对聚合根,不针对聚合内实体:
java
// ✅ 正确:只提供 OrderRepository
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
}
// ❌ 错误:提供 OrderItemRepository
public interface OrderItemRepository {
void save(OrderItem item); // 加了等于绕过聚合根
}加载 Order 时,内部的 OrderItem 自动加载(同一 SQL JOIN 即可);要查 OrderItem,只能通过 Order。
五、跨聚合协调:领域事件
订单支付后,要扣库存、发通知、发积分——不能塞同一事务:
java
@Transactional
public class OrderApplicationService {
@Resource private OrderRepository orderRepository;
@Resource private ApplicationEventPublisher publisher;
public void pay(OrderId orderId, Money amount) {
// 1. 改订单状态(订单聚合内)
Order order = orderRepository.findById(orderId);
order.pay(amount);
orderRepository.save(order);
// 2. 发领域事件,异步处理其他聚合
order.pullEvents().forEach(publisher::publishEvent);
}
}
// 库存上下文订阅
@EventListener
@Transactional
public void onOrderPaid(OrderPaidEvent event) {
inventoryService.deduct(event.skuIds(), event.quantities());
}⚠️ 坑 3:用
@Transactional把订单 + 库存 + 积分全包了,3 个表全部回滚。Saga/事件才是正解。
六、聚合根的生命周期
java
public class Order {
public static Order create(BuyerId buyerId, Address address) {
// 工厂方法集中创建逻辑
OrderId id = OrderId.generate();
Instant now = Instant.now();
Order order = new Order(id, buyerId, address, now);
order.addDomainEvent(new OrderCreatedEvent(id, buyerId, now));
return order;
}
private Order(OrderId id, BuyerId buyerId, Address address, Instant createdAt) {
this.id = id;
this.buyerId = buyerId;
this.shippingAddress = address;
this.createdAt = createdAt;
this.status = OrderStatus.CREATED;
}
}构造函数私有,必须走 create() 工厂方法,保证不变量初始成立。
⚠️ 坑 4:
new Order()满大街散布,业务规则无法统一。强制工厂方法 才能保证对象创建时一定合法。
本章小结
| 原则 | 做法 |
|---|---|
| 聚合要小 | 只放必须一起改的实体 |
| 聚合根唯一入口 | 私有构造 + 工厂方法 |
| 一致性边界 | 聚合内强一致,聚合外最终一致 |
| 仓储只对聚合根 | 不为内部实体提供仓储 |
| 跨聚合异步 | 领域事件 + Saga |
| 不可变快照 | 跨聚合用 ID 引用 |
动手练习
- 聚合设计:为"购物车"设计聚合,包含
Cart、CartItem,给出字段和业务方法 - 不变量:实现
Order.cancel()校验"已发货订单不能取消" - 事件发布:在
Order.markPaid()里发出OrderPaidEvent,并在应用服务里发布 - 仓储隔离:写
OrderRepository接口,只暴露findById / save / findByBuyer,不允许按 item 查询
下一章:第 6 章:领域服务 →