← 返回题目列表

使用建造者模式有哪些常见误区?

高频 中等 第 6 / 26 题 更新于 2026/07/28
建造者模式设计误区过度设计代码质量

简化版

建造者模式常见误区包括:把链式 setter 当 Builder、对象很简单也强行使用、build() 不做校验、最终对象仍然随意可变、Builder 被多线程共享。它的目标是管理复杂构建,不是给所有对象套一层模式。

详细版

常见误区可以分为几类。

第一,把链式调用等同于 Builder。真正的 Builder 应该有构建阶段和最终对象阶段,通常通过 build() 收口。

第二,过度设计。两个字段的小对象没必要写 Builder:

new Point(x, y);

这比 Point.builder().x(x).y(y).build() 更直接。

第三,build() 只是机械创建对象,没有校验、默认值、防御性拷贝。这样 Builder 只剩语法糖,没发挥设计价值。

第四,Product 仍然暴露大量 setter。这样对象创建后仍然可以被改坏,不利于状态一致性。

第五,复用 Builder 实例导致脏数据:

User.Builder builder = User.builder().role("ADMIN");
User a = builder.name("A").build();
User b = builder.name("B").build(); // 可能意外继承 role

Builder 通常应该是临时对象,用完即丢。

第六,把 Director、抽象 Builder、具体 Builder 全部机械写出来,但业务里没有固定构建流程,导致类数量膨胀。

完整版教学

一、误区一:只看形式,不看目的

很多代码长得像 Builder:

new User().name("Tom").age(18);

但它可能只是链式 setter。判断是不是有建造者模式的价值,应该看它有没有解决这些问题:

  • 是否减少了长构造器的可读性问题;
  • 是否封装了默认值和校验;
  • 是否避免半初始化对象泄露;
  • 是否让最终对象状态更稳定;
  • 是否把复杂构建过程集中管理。

如果只是把 setter 改成返回 this,没有 build() 收口,也没有构建规则,那只是语法上的链式调用。

二、误区二:简单对象也强行 Builder

模式不是越多越高级。比如:

public record Point(int x, int y) {}

这个对象字段少、语义清楚、构造简单,直接构造就够了。

如果写成:

Point.builder().x(10).y(20).build();

反而让代码噪音变多。

判断是否需要 Builder,可以问三个问题:

  • 字段是否很多?
  • 可选参数是否很多?
  • 构建规则是否复杂?

如果三个答案都是否,通常不需要 Builder。

三、误区三:忽略对象不变量

对象不变量指对象创建后必须一直成立的规则。例如:

  • 价格不能为负;
  • 开始时间不能晚于结束时间;
  • HTTPS 配置必须有证书;
  • 分页大小必须在合理范围内。

如果 Builder 不校验这些规则,就可能创建出坏对象:

Order.builder()
        .amount(-100)
        .build();

一个好的 Builder 应该在 build() 中阻止这种对象生成:

if (amount.compareTo(BigDecimal.ZERO) < 0) {
    throw new IllegalArgumentException("amount must not be negative");
}

建造者模式的价值不是“让创建看起来优雅”,而是“让非法对象更难出现”。

四、误区四:Builder 复用导致状态污染

Builder 是可变对象。如果重复使用同一个 Builder,很容易把上一次的字段带到下一次。

Notification.Builder builder = Notification.builder()
        .channel("email");

Notification n1 = builder.to("a@example.com").build();
Notification n2 = builder.to("b@example.com").build();

如果中间设置了某个字段但下一次忘了清理,就会出现隐蔽 bug。

更推荐每次创建新的 Builder:

Notification n1 = Notification.builder().channel("email").to("a@example.com").build();
Notification n2 = Notification.builder().channel("email").to("b@example.com").build();

如果确实要复用模板,可以提供 toBuilder() 或复制方法,但要明确语义。

五、误区五:机械照搬经典 UML

有些人会为每个对象都写:

  • 抽象 Builder;
  • 具体 Builder;
  • Director;
  • Product;
  • 一堆步骤方法。

但如果业务里只有一个产品、一套构建逻辑、没有固定流程复用,这些类可能只是增加维护成本。

现代 Java 里最常见的 Builder 其实很轻:

Product.builder().field(value).build();

是否需要抽象 Builder 和 Director,要看有没有“多种构建实现”和“固定流程复用”。

六、用一个非法订单识别“只有链式外壳”的 Builder

订单要求 amount>0,数字商品不允许 shippingAddress,实物商品则地址必填。如果链式 API 最后仍能构造 amount=-100 或实物无地址的 Order,这个 Builder 只改善了写法,没有守住对象不变量。

Order.builder()
  .type(PHYSICAL)
  .amount(99.00)
  .shippingAddress("Shanghai")
  .items(mutableItems)
build(): 检查必填字段
build(): 检查 type 与 address 组合
build(): List.copyOf(items)
new Order(validatedSnapshot)
返回不可变合法成品

这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。

七、构建契约与对象不变量

评审维度本题结论
必填信息订单类型、正金额、至少一个条目;实物订单还需要地址。
默认值币种等默认值在 Builder 内集中设置,不由调用方到处补。
跨字段规则商品类型与配送地址、金额与折扣等组合在 build() 检查。
可变引用处理集合用 List.copyOf 或防御性拷贝,Product 不暴露可变内部集合。
Builder 生命周期短生命周期、局部使用、默认一次构建后丢弃。
Product 交付保证字段固定且所有业务不变量在返回前成立。

build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。

八、与构造器、JavaBean 和工厂的选择边界

  • 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
  • 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
  • JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
  • Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
  • 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
  • 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
  • 链式 return this 只是语法,校验、默认值、拷贝和收口才是设计语义。
  • 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。

若对象只有 x、y 两个明确字段,new Point(x,y) 或 record 更清楚;若只是链式 setter 且 Product 仍任意可变,不要把它宣传为完整 Builder 设计。

九、交付前的代码与测试检查

  1. 缺少每个必填字段分别调用 build(),应得到明确且稳定的异常。
  2. 完全不设置可选字段,核对默认值来自唯一权威位置。
  3. 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
  4. 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
  5. 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
  6. 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
  7. 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
  8. 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
  9. 若使用 Lombok,检查生成代码、默认值、@Singular、构造器可见性和框架兼容。
  10. 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。

记忆钩子:Builder 的价值看非法对象能否被挡在 build() 之前,不看调用链有多漂亮。

十、常见误区与追问

  • 误区:方法返回 this 就是建造者模式。 那只能证明支持链式调用,未必存在构建收口和不变量。
  • 误区:简单对象使用 Builder 一定更专业。 额外类型和调用噪音可能超过收益。
  • 误区:Product 字段 final 就一定不可变。 final 只固定引用,可变集合内容仍可能被外部修改。
  • 追问:Builder 能否复用? 默认不建议;若支持模板复制,应提供 toBuilder 或每次返回新 Builder。
  • 追问:Director 一定需要吗? 不一定,只有固定构建流程需要复用时才有明显价值。
  • 追问:校验放哪里? 单字段可早校验,跨字段在 build 收口,Product 构造保留必要兜底。

十一、加强记忆

建造者模式的坑主要来自两个方向:该用时没把校验和状态封住,不该用时又过度设计。记住它服务的是复杂构建;简单对象别硬套,复杂对象别只写链式壳子。