Skip to content

Aggregate(聚合) ​

解决什么问题:在多个强关联的对象里,挑一个做"入口",让其它对象通过它访问—— 没有聚合 = 谁都能改任何对象,并发冲突、业务规则散落各处、一致性无法保证。 聚合 = 划定"一致性边界"——边界内强一致、边界外最终一致。

它是什么 ​

聚合(Aggregate) 是 DDD 战术层的核心模式——一组必须保持强一致性的领域对象,作为一个整体被访问和修改的单元。

每个聚合有:

  • 聚合根(Aggregate Root)——外部唯一能引用的对象
  • 内部实体 / 值对象——只能通过根访问
聚合 Order {                ← 根:Order
  id, customerId, status     ← 外部只能引用 Order.id
  items: List<OrderItem>     ← 内部实体:通过 Order.items 修改
  shippingAddress: Address   ← 值对象:不可变
}

Vernon Ch 10: "An aggregate is a cluster of domain objects that can be treated as a single unit... An aggregate will have one root entity (the aggregate root) and any number of subordinate entities and value objects."

V2 视角(V2.7 B3 段 3 "分解的最小单元"):聚合是 L1 战术层的"最小分解单元"——比类大,比 BC 小。一个 BC 由 N 个聚合组成,聚合之间靠 ID 引用、不持对象引用。

关键属性 ​

属性值/含义
边界一个聚合 = 一个一致性边界
根Aggregate Root,唯一对外引用点
引用聚合间用 ID 引用,不持对象引用
事务一个事务 = 一个聚合的修改(不能跨聚合事务)
大小越小越好——越小 = 并发越高 = 性能越好
不变式根负责守护整个聚合的业务规则
仓储一个聚合 = 一个 Repository(根的仓储)
删除删除根 = 整个聚合级联删除

怎么用 ​

场景 1:识别聚合(从业务事件反推) ​

事件风暴产物:
- OrderPlaced { orderId, items, customerId }
- OrderItemAdded { orderId, itemId, qty }
- OrderPaid { orderId, amount }
- OrderShipped { orderId, address }

问一个问题:哪些对象必须"同时修改"才不破坏业务规则?

  • Order + OrderItem → 必须同事务(先加 item 后下单 = 数据不一致)
  • Order + Payment → 异步解耦(不要塞一个聚合里)
  • Order + ShippingAddress → 同聚合(订单地址跟着订单走)

结论:Order + OrderItem + ShippingAddress = 一个聚合。

场景 2:聚合根的代码骨架(Java 示例) ​

java
public class Order {                          // 聚合根
  private OrderId id;
  private CustomerId customerId;             // 跨聚合用 ID
  private List<OrderItem> items;
  private ShippingAddress address;           // 值对象

  // 工厂方法——保证创建即合法
  public static Order place(CustomerId cid,
                            Cart cart,
                            ShippingAddress addr) {
    if (cart.isEmpty()) throw new EmptyCartException();
    return new Order(OrderId.generate(), cid, cart.toItems(), addr);
  }

  // 业务行为——内部一致性由根守护
  public void addItem(Product p, int qty) {
    if (status != OrderStatus.DRAFT)
      throw new OrderAlreadyPlacedException();
    items.add(new OrderItem(p.id(), qty, p.price()));
  }

  public Money total() {
    return items.stream()
                .map(OrderItem::subtotal)
                .reduce(Money.ZERO, Money::add);
  }

  // 不暴露 setter——只能通过业务行为修改
}

场景 3:跨聚合的最终一致(最常见的坑) ​

java
// 错误:跨聚合事务
@Transactional
public void placeOrderAndReserveStock(Order order) {
  orderRepo.save(order);
  stockRepo.reserveAll(order.items());  // ← 跨聚合事务 = 分布式锁
}

// 正确:发领域事件,本聚合内强一致,跨聚合最终一致
public class Order {
  public void place() {
    this.status = OrderStatus.PLACED;
    DomainEventPublisher.publish(
        new OrderPlaced(this.id, this.items)
    );
  }
}

// 库存 BC 订阅事件,异步扣减
@EventListener
public void on(OrderPlaced e) {
  stockService.reserveFor(e.orderId, e.items);
}

Vernon Ch 10 反复强调:"一个事务只改一个聚合"——这不是性能限制,是设计原则。

场景 4:聚合大小的权衡 ​

V2 B6 段 5 视角(折衷):

  • 太大:并发差(一个用户下单锁住整个订单系统)、性能差、难重构
  • 太小:业务规则散落、需要大量应用层协调、最终一致难维护

判定标准:能不能用一句话说清"这个聚合守护的不变式是什么"。

  • "Order 聚合守护:未支付的订单必须有未过期的商品报价" → ✓ 清晰
  • "Order+Payment+Shipping 守护:付款后必须立即发货" → ✗ 三个不同生命周期

常见误区 ​

  1. "聚合越大越完整"——错。大聚合 = 锁竞争 = 性能差。能用 ID 引用就别包进来
  2. "跨聚合用事务保证强一致"——错。Vernon Ch 10:跨聚合 = 最终一致 + 领域事件。强一致只能在一个聚合内
  3. "聚合根就是 Entity"——错。聚合根是 Entity,但 Entity 不一定是根——只有"外部引用入口"才叫根
  4. "暴露 setter 让外部改内部对象"——错。暴露 setter = 绕过不变式检查 = 数据脏。只能通过业务行为修改
  5. "一个 BC 一个聚合"——错。一个 BC 通常 5-15 个聚合。一个聚合 = 一个一致性单元

关键洞察 ​

聚合 = "事务边界" + "不变式边界" + "并发边界"——三合一。

划清聚合 = 划清了"强一致到哪儿为止"。在这之内强一致(同步、事务), 在这之外最终一致(异步、事件)。这是分布式系统设计的最关键决策—— 比微服务拆分、消息队列选型都更基础。

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。