← 返回题目列表

如何用 Builder 构建复杂聚合对象?和普通对象 Builder 有什么区别?

高频 困难 第 13 / 26 题 更新于 2026/08/01
建造者模式聚合对象领域模型复杂对象构建

简化版

复杂聚合对象的 Builder 不只是收集字段,还要管理子对象、集合、默认规则、跨对象一致性和防御性拷贝。它适合订单、报表、查询条件、消息模板这类由多个部分组成的对象,但要避免把业务流程都塞进 Builder,导致 Builder 变成过重的“组装服务”。

详细版

普通对象 Builder 主要解决参数多、可选项多的问题;复杂聚合 Builder 解决的是“多个部分如何一致地组装成一个整体”。比如订单对象可能包含:

  • 订单基础信息;
  • 买家和收货地址;
  • 多个订单明细;
  • 优惠、运费、税费;
  • 支付方式;
  • 扩展属性。

这时 Builder 的职责通常包括:

  • 提供 addItem()address()coupon() 这类贴近业务的构建方法;
  • build() 里检查明细不能为空、金额不能为负、优惠不能超过商品金额;
  • 对集合和可变对象做防御性拷贝;
  • 保证最终聚合对象创建后处于一致状态;
  • 不把外部服务调用、库存扣减、支付创建等业务动作塞进 Builder。

面试要强调边界:Builder 负责构建内存对象,领域服务负责执行业务流程。如果 Builder 里开始查数据库、发消息、调用远程服务,它就偏离了创建型模式的定位。

完整版教学

一、复杂聚合对象为什么比普通对象难构建

普通 Builder 可能只是把 8 个字段收集起来,然后调用构造器。复杂聚合对象不同,它内部常常有多个子对象和集合,并且这些部分之间存在一致性约束。比如订单聚合中,订单总金额必须等于明细金额、优惠、运费和税费计算后的结果。

Order
  ├─ Buyer
  ├─ Address
  ├─ List<OrderItem>
  ├─ Discount
  └─ PaymentPlan

如果让调用方自己 new 这些子对象,再手动计算总价,很容易出现半成品对象。Builder 的价值在于提供一个统一施工区:调用方逐步放入部件,build() 负责最后检查和冻结。

记忆钩子:普通 Builder 管“字段太多”,聚合 Builder 管“部件之间必须拼得上”。

二、聚合 Builder 常见 API 形态

复杂聚合 Builder 的方法通常比简单 setter 更贴近业务动作。比如构建订单时,addItem(skuId, quantity, price)items(list) 更安全,因为 Builder 可以在添加明细时做局部校验,也能隐藏 OrderItem 的内部构造细节。

Order order = Order.builder()
        .buyer("U1001")
        .shipTo("Shanghai", "Pudong")
        .addItem("SKU-1", 2, 19900)
        .addItem("SKU-2", 1, 9900)
        .coupon(5000)
        .freight(1200)
        .build();

这种 API 的优点是调用方不需要知道订单明细对象怎么创建,也不需要自己维护金额计算。Builder 对外暴露的是“我要给订单加一个商品”,而不是“我要操作某个内部 List”。这能减少调用方绕过规则的机会。

三、用数字例子理解跨部件一致性

假设订单有两件商品:SKU-1 单价 199 元、数量 2;SKU-2 单价 99 元、数量 1;优惠 50 元,运费 12 元。用分为单位计算:

itemsTotal = 19900 * 2 + 9900 * 1 = 49700
coupon = 5000
freight = 1200
payable = 49700 - 5000 + 1200 = 45900

如果这些计算散落在调用方,10 个调用点可能出现 10 套金额规则。Builder 可以把这条规则收进 build(),让最终对象里的 payableAmount 总是和明细一致。

public Order build() {
    if (items.isEmpty()) {
        throw new IllegalStateException("order items required");
    }
    long itemsTotal = items.stream()
            .mapToLong(item -> item.unitPrice() * item.quantity())
            .sum();
    if (couponAmount > itemsTotal) {
        throw new IllegalStateException("coupon exceeds item total");
    }
    long payable = itemsTotal - couponAmount + freightAmount;
    return new Order(buyerId, address, List.copyOf(items), itemsTotal, couponAmount, freightAmount, payable);
}

这里 Builder 不只是“把字段传给构造器”,它在交付对象前完成了聚合一致性检查。

四、集合和子对象为什么必须防御性拷贝

复杂聚合对象最容易踩的坑是集合泄漏。调用方传入一个 List<OrderItem>,Builder 直接保存引用,最终 Product 也直接持有这个引用。这样对象构建完成后,外部代码仍然可以修改明细。

List<OrderItem> items = new ArrayList<>();
items.add(new OrderItem("SKU-1", 2, 19900));

Order order = Order.builder().items(items).build();
items.clear(); // 如果没有拷贝,order 内部明细可能也被清空

稳妥做法是在 Builder 接收集合时复制一次,在 Product 构造时再冻结一次。对于数组、Map、可变子对象,也要按同样思路处理。复杂聚合 Builder 的交付目标不是“拼出对象”,而是“拼出不会被外部引用继续污染的对象”。

输入类型风险推荐处理
List/Set外部增删元素new ArrayList<>(input) + List.copyOf
Map外部改键值复制并冻结
byte[]外部改数组内容Arrays.copyOf
可变子对象内部状态被改使用不可变值对象或复制

五、Builder 和领域服务的职责边界

复杂聚合 Builder 很容易越界。比如订单 Builder 如果开始查库存、锁优惠券、创建支付单、发送消息,它就不再只是对象构建器,而变成了业务编排器。这样会带来测试困难和副作用不可控的问题。

更清楚的分工是:

Builder:
  收集构建参数
  创建子对象
  校验内存内一致性
  防御性拷贝

Domain Service / Application Service:
  查库存
  校验用户状态
  锁定优惠券
  调用支付
  发布领域事件

Builder 可以验证“明细不能为空、金额不能为负、优惠不能超过商品金额”,但不应该验证“这个 SKU 当前库存是否足够”,因为库存需要外部数据源。把外部依赖放进 Builder,会让一个原本简单的创建模式变成难测的服务对象。

六、什么时候需要子 Builder

如果聚合内部子对象也很复杂,可以为子对象提供独立 Builder,再由聚合 Builder 接收成品子对象或提供嵌套构建入口。例如报表对象包含筛选条件、排序规则、展示列、导出格式,每个部分都有自己的规则。

ReportBuilder
  .filter(Filter.builder().eq("status", "PAID").between("amount", 100, 500).build())
  .sort(Sort.by("createdAt").desc())
  .column("orderId")
  .column("amount")
  .build()

是否拆子 Builder,看两个指标:子对象能否独立复用,子对象规则是否足够复杂。如果只是两个字符串,拆出来会显得啰嗦;如果子对象自己有 5 个字段、3 条组合规则,就值得独立建模。

七、聚合 Builder 的测试重点

复杂聚合 Builder 的测试不能只测“能 build 成功”。要覆盖正常路径、缺失部件、金额边界、集合不可变、外部引用污染和非法组合。

测试清单:
1. 0 个 item -> build 失败
2. coupon = itemsTotal + 1 -> build 失败
3. freight = 0 -> 可通过
4. freight = -1 -> build 失败
5. 构建后修改原始 items -> order.items 不变
6. order.items().add(...) -> 抛异常或无法修改
7. 两个 item 金额合计 49700,coupon 5000,freight 1200 -> payable 45900

这些测试能证明 Builder 真正承担了聚合一致性边界,而不只是让调用代码看起来更顺。

八、常见误区与追问

  • 误区:复杂聚合 Builder 只是普通字段 Builder 的放大版。 它还要处理子对象、集合、跨部件规则和冻结边界。
  • 误区:Builder 里可以顺手查数据库。 外部依赖和副作用属于服务层,放进 Builder 会让构建过程难以预测。
  • 误区:Product 构造后集合不会被改。 如果没有防御性拷贝,外部引用仍可能修改内部状态。
  • 追问:金额计算放 Builder 还是 Order 内部? 构建时派生字段可由 Builder 计算,核心不变量也可以在 Product 构造器兜底。
  • 追问:什么时候拆子 Builder? 子对象规则复杂且可复用时拆;简单值对象不必过度拆分。
  • 追问:聚合 Builder 是否等同于领域服务? 不等同,Builder 负责内存对象组装,领域服务负责跨资源业务流程。
  • 追问:如何防止 Builder 过重? 保持无外部副作用,只做构建参数、局部规则、聚合一致性和不可变交付。

九、加强记忆

复杂聚合 Builder 要记住“组装、校验、冻结、别越界”。它组装多个部件,校验跨部件一致性,冻结集合和可变引用,但不承担查库、扣库存、发消息这类业务编排。能说清这条边界,才算真正理解 Builder 在工程里的位置。