Administrator
发布于 2022-09-29 / 8209 阅读
218

DDD 聚合设计实战:从订单系统说起

订单系统重构:一个"订单"对象膨胀到了 3000 行

我们老订单系统里有个 Order 类,承载了下单、改地址、退款、发票、物流查询……所有逻辑,文件 3000 多行,改一处怕动全身。重构时我用 DDD 的聚合(Aggregate)思想重新切分,核心问题其实就一个:聚合边界划在哪。划错了,要么事务跨太大,要么一致性保不住。

聚合是什么:一致性边界,不是数据边界

聚合是一组必须保持内部一致的业务对象的集合,对外只暴露一个聚合根(Aggregate Root)。划聚合的本质,是划一致性边界:边界内的对象一起变、一起校验、一起提交;边界外的通过 ID 引用,不直接持有对象。

订单域里,我先列出候选实体:订单(Order)、订单项(OrderItem)、支付记录(Payment)、收货地址(Address)、用户(User)、发票(Invoice)。哪些该进一个聚合?

边界划分:以"是否必须同事务一致"为准

关键判据:两个对象是否需要在同一个业务规则下同时保持一致?

  • Order + OrderItem:订单总金额必须等于所有订单项之和,删一个订单项要重算总额——强一致,放进同一聚合,Order 是聚合根。
  • Address:地址可以被多个订单引用,且地址变更不应该回溯改历史订单——独立聚合,订单里只存 addressId
  • Payment:支付有独立生命周期(可能多次支付、退款),且支付系统通常是另一个限界上下文——独立聚合,通过事件(订单已支付)关联。
  • User:明显是另一个聚合根,订单只持有 userId
// 聚合内:Order 是根,OrderItem 是内部实体,随根一起持久化
public class Order {                 // 聚合根
    private OrderId id;
    private List<OrderItem> items;   // 内部实体,非独立
    private Money total;

    public void addItem(Product p, int qty) {
        items.add(new OrderItem(p, qty));
        recalcTotal();               // 内部保证一致性
    }
    private void recalcTotal() {
        total = items.stream()
                     .map(i -> i.price().multiply(i.qty()))
                     .reduce(Money.ZERO, Money::add);
    }
}

// 跨聚合:只持有 ID,不持有对象
public class Order {
    private UserId buyerId;         // 不是 User 对象
    private AddressId addressId;    // 不是 Address 对象
}

事务范围的确定:一个事务 = 一个聚合

DDD 的硬规矩:一个事务只修改一个聚合。修改跨聚合的一致性,靠领域事件最终一致,而不是一个大事务。

比如"支付成功后扣减库存":订单聚合在支付状态变更时发出 OrderPaid 事件,库存聚合监听到后自己扣减。两者不在同一事务,订单库和库存库甚至可以分库。

边界决策错误做法正确做法
Order 与 OrderItem拆成两个聚合,靠应用层组装同一聚合,根保证总额一致
Order 与 Payment一个大事务一起提交两个聚合,事件驱动
Order 与 User订单持有 User 对象只持有 userId

踩过的两个坑

  • 聚合过大:最初我把支付也塞进订单聚合,结果下单事务要锁支付记录,并发下锁冲突严重。拆出去后用事件,吞吐翻倍。
  • 聚合过小:曾把 OrderItem 单独做成聚合根,导致"加购时校验库存+订单总额"要在应用层跨两个聚合拼事务,一致性反而难保。还是放回订单聚合内最自然。

就写到这。如果哪天你也被《DDD 聚合设计实战:从订单系统说起》里同一个坑绊住,回来翻这篇,能省半小时。

参考