使用建造者模式有哪些常见误区?
简化版
建造者模式常见误区包括:把链式 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 设计。
九、交付前的代码与测试检查
- 缺少每个必填字段分别调用
build(),应得到明确且稳定的异常。 - 完全不设置可选字段,核对默认值来自唯一权威位置。
- 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
- 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
- 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
- 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
- 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
- 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
- 若使用 Lombok,检查生成代码、默认值、
@Singular、构造器可见性和框架兼容。 - 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。
记忆钩子:Builder 的价值看非法对象能否被挡在 build() 之前,不看调用链有多漂亮。
十、常见误区与追问
- 误区:方法返回 this 就是建造者模式。 那只能证明支持链式调用,未必存在构建收口和不变量。
- 误区:简单对象使用 Builder 一定更专业。 额外类型和调用噪音可能超过收益。
- 误区:Product 字段 final 就一定不可变。 final 只固定引用,可变集合内容仍可能被外部修改。
- 追问:Builder 能否复用? 默认不建议;若支持模板复制,应提供 toBuilder 或每次返回新 Builder。
- 追问:Director 一定需要吗? 不一定,只有固定构建流程需要复用时才有明显价值。
- 追问:校验放哪里? 单字段可早校验,跨字段在 build 收口,Product 构造保留必要兜底。
十一、加强记忆
建造者模式的坑主要来自两个方向:该用时没把校验和状态封住,不该用时又过度设计。记住它服务的是复杂构建;简单对象别硬套,复杂对象别只写链式壳子。