如何设计出合理的聚合根

32 阅读7分钟

你想知道在DDD中如何设计出合理的聚合根,核心是要找准聚合的边界、明确聚合根的职责,并通过聚合根保障业务规则和数据一致性,避免聚合过大或过小、职责混乱等常见问题。

设计合理的聚合根没有绝对的标准答案,但有一套经过实践验证的核心原则和步骤,下面我会从识别原则、设计步骤、实战技巧、避坑指南四个维度,结合具体例子帮你掌握设计方法。

一、核心识别原则(先判断“谁能当”聚合根)

设计聚合根的第一步,是先从领域模型中筛选出符合聚合根特征的实体,核心原则有5个:

  1. 有独立的生命周期
    聚合根可以独立存在,聚合内的其他对象(实体/值对象)必须依赖它。
    ✅ 正确示例:订单(Order) 可独立存在,订单项(OrderItem) 不能脱离订单;
    ❌ 错误示例:把“收货地址”设为聚合根,因为地址依赖于“用户”或“订单”,无独立生命周期。

  2. 有全局唯一的标识
    聚合根的ID是跨系统/跨聚合唯一的(如UUID、自增ID),而聚合内其他实体的ID仅需在聚合内唯一(如订单项的序号)。

  3. 承担核心业务规则
    聚合内的核心业务逻辑(如库存校验、订单状态流转)必须由聚合根封装,而非外部调用者。
    ✅ 正确示例:Order.AddItem() 方法内校验“商品库存≥购买数量”;
    ❌ 错误示例:外部代码先查库存,再直接给Order.Items加订单项(易导致并发下数据不一致)。

  4. 是外部访问的唯一入口
    外部对象只能引用聚合根,不能直接引用聚合内的其他对象。
    ✅ 正确示例:通过OrderRepository.GetById(orderId)获取订单,再从订单中取订单项;
    ❌ 错误示例:提供OrderItemRepository.GetById(itemId)直接查询订单项。

  5. 能保证聚合内数据一致性
    聚合根要确保聚合内所有对象的状态符合业务规则,一次操作要么全部成功,要么全部失败(无需分布式事务,聚合内是内存操作)。

二、落地设计步骤(从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));
    }
}

三、实战设计技巧

  1. 聚合要“小而精”
    一个聚合只解决一个核心业务问题,避免“大聚合”(如把“用户+订单+收货地址”都放进一个聚合)。
    ✅ 推荐:用户聚合(User)、订单聚合(Order)、商品聚合(Product)各自独立;
    ❌ 避免:Order聚合中包含完整的User对象(只需保存UserId即可)。

  2. 跨聚合关联用ID,不用对象引用
    聚合根之间通过ID关联,而非直接持有对象,降低耦合:
    ✅ 正确:Order 中保存 UserId,而非 User 对象;
    ❌ 错误:Order 中直接持有 User 对象(会导致两个聚合耦合,且持久化时易出问题)。

  3. 值对象优先,减少实体数量
    聚合内无独立生命周期、无业务行为的对象,优先设计为值对象(如地址、金额、商品规格),值对象不可变,能减少状态管理的复杂度。

  4. 通过工厂方法创建聚合根
    私有化聚合根的构造函数,通过静态工厂方法创建,确保创建时就满足所有业务规则(如必填项、初始状态)。

四、常见避坑指南

常见错误问题后果正确做法
聚合根只存属性,无行为业务规则散落在服务层,易导致数据不一致把核心规则封装到聚合根的方法中
聚合过大(包含过多对象)性能差、维护难、并发冲突多拆分小聚合,通过ID关联
外部直接修改聚合内对象绕过业务规则,数据混乱聚合内对象私有化,仅通过聚合根方法修改
聚合根之间直接引用对象耦合高,持久化复杂仅保存对方的ID,需要时通过仓储查询
为聚合内的每个对象设计仓储破坏聚合边界,无法保障一致性仅为聚合根设计仓储

总结

设计合理的聚合根,核心要抓住3个关键点:

  1. 边界清晰:聚合只包含完成一个核心业务功能的对象,聚合要小,跨聚合用ID关联;
  2. 行为封装:聚合根是业务规则的载体,所有对聚合内对象的修改必须通过聚合根的方法,而非直接操作属性;
  3. 入口唯一:外部只能通过聚合根访问聚合内的对象,仓储仅针对聚合根设计,保障数据一致性。

简单来说,判断聚合根设计是否合理的终极标准:是否能在不依赖外部事务的情况下,保障聚合内所有业务规则的完整性和数据的一致性

本文使用 文章同步助手 同步