什么是建造者模式?它解决什么问题?
简化版
建造者模式是一种创建型设计模式,用来把复杂对象的创建过程拆成多个清晰步骤,最后统一生成对象。它主要解决构造参数过多、构造过程复杂、对象创建逻辑和业务代码混在一起的问题。
详细版
建造者模式适合创建“字段多、组合多、构建规则多”的对象。它不强调“创建哪一种产品”,而强调“怎样一步步把一个复杂产品组装出来”。
常见使用场景有:
- 一个对象有很多可选参数,构造方法越写越长;
- 对象创建前需要做参数校验、默认值填充、派生字段计算;
- 创建过程需要按步骤表达,例如配置连接池、构建 HTTP 请求、组装复杂报表;
- 希望创建出来的对象不可变,避免后续被随意修改;
- 希望调用代码更可读,例如
builder().host(...).port(...).timeout(...).build()。
典型结构一般包括:
Product:最终要创建的复杂对象;Builder:负责分步骤设置属性和构建规则;build():统一做校验、默认值处理,并返回最终对象;Director:可选角色,用来封装固定构建流程,现代业务代码里不一定出现。
一个很典型的 Java 写法是:
public class HttpRequest {
private final String url;
private final String method;
private final int timeoutMs;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.timeoutMs = builder.timeoutMs;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String url;
private String method = "GET";
private int timeoutMs = 3000;
public Builder url(String url) {
this.url = url;
return this;
}
public Builder method(String method) {
this.method = method;
return this;
}
public Builder timeoutMs(int timeoutMs) {
this.timeoutMs = timeoutMs;
return this;
}
public HttpRequest build() {
if (url == null || url.isBlank()) {
throw new IllegalArgumentException("url required");
}
return new HttpRequest(this);
}
}
}
调用方会变得比较清楚:
HttpRequest request = HttpRequest.builder()
.url("https://example.com")
.method("POST")
.timeoutMs(5000)
.build();
面试回答时可以强调:建造者模式不是为了少写代码,而是为了把复杂对象的创建过程表达清楚,并把构建规则集中到一个地方。
完整版教学
一、为什么需要建造者模式
最常见的起点是“构造方法爆炸”。比如一个对象有十几个字段,其中一部分必填,一部分可选,一部分有默认值,一部分还需要互相校验。刚开始可能会写成这样:
new Config("localhost", 8080, true, 3000, 10, null, false);
这种代码有三个问题。
第一,可读性差。调用方看到一串参数,很难知道 true、3000、10 分别代表什么。
第二,容易传错。尤其是多个参数类型相同的时候,编译器无法帮你发现顺序错误。
第三,构建规则分散。默认值、合法性校验、字段依赖关系可能散落在多个构造方法或调用方里,后期维护成本会越来越高。
建造者模式把创建动作变成“带名字的步骤”:
Config config = Config.builder()
.host("localhost")
.port(8080)
.sslEnabled(true)
.timeoutMs(3000)
.build();
这段代码看起来更长,但信息密度更高,维护风险更低。
二、建造者模式的核心思想
建造者模式的核心是“分离复杂对象的构建过程和最终表示”。
“构建过程”指的是:设置字段、应用默认值、检查参数、处理字段之间的依赖、按步骤组装对象。
“最终表示”指的是:创建完成后的对象本身。对象一旦被创建出来,最好只负责承载状态和提供业务行为,而不是到处暴露半成品状态。
因此,一个好的 Builder 通常会把这些逻辑收进去:
- 必填字段检查;
- 默认值设置;
- 字段范围校验;
- 字段组合合法性校验;
- 最终对象创建。
这也是 build() 方法重要的原因。它不是简单 return new Product(...),而是构建过程的“收口点”。
三、它和普通构造方法有什么区别
普通构造方法适合字段少、规则简单的对象。例如:
new Point(10, 20);
这类对象用 Builder 反而啰嗦。
建造者模式适合字段多、可选项多、规则复杂的对象。例如 HTTP 请求、数据库连接配置、消息发送配置、搜索条件、报表导出参数等。
对比一下:
| 方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 构造方法 | 简单直接 | 参数多时可读性差 | 字段少、规则简单 |
| JavaBean setter | 写法灵活 | 对象可能处于半初始化状态 | 框架绑定、简单 DTO |
| Builder | 可读、可校验、可构建不可变对象 | 代码量更多 | 复杂对象、可选参数多 |
所以建造者模式不是替代所有构造方法,而是解决“复杂创建”这个特定问题。
四、面试时容易漏掉的重点
很多人会把建造者模式答成“链式调用”。这是不完整的。链式调用只是常见写法,真正关键是构建过程的封装。
例如下面这种代码虽然是链式的,但不一定是严格意义上的 Builder:
user.setName("Tom").setAge(18);
如果它只是不断修改同一个可变对象,没有统一校验,没有最终 build() 收口,也没有解决复杂构建问题,那它更像链式 setter。
真正的 Builder 通常有一个独立的构建器对象,先收集构建参数,最后调用 build() 创建最终对象。
五、用七参数 HTTP 请求推导 Builder 的必要性
HTTP 请求同时包含 url、method、timeout、headers、body、TLS 和代理配置。直接构造器中的多个 String/int/boolean 容易错位,JavaBean 又会暴露只有 url 没有 method/body 的半成品;Builder 用命名步骤收集信息,再一次性交付合法请求。
HttpRequest.builder()
.url("https://api.example.com/orders")
.method(POST)
.timeoutMs(5000)
.header("Idempotency-Key", "K-42")
.body(jsonBytes)
.tlsEnabled(true)
build(): url 必填、timeout>0
build(): POST body 与 TLS 配置组合校验
build(): 复制 headers/body
new HttpRequest(validatedSnapshot)
调用方只看到完整成品
这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。
六、构建契约与对象不变量
| 评审维度 | 本题结论 |
|---|---|
| 必填信息 | url 等执行请求不可缺少的信息。 |
| 默认值 | method=GET、timeout=3000 等在 Builder 中集中声明。 |
| 跨字段规则 | method/body、TLS/证书、proxy host/port 等组合在 build 检查。 |
| 可变引用处理 | headers Map 与 body 数组构建时复制,防止调用方后改。 |
| Builder 生命周期 | Builder 是单次请求配置施工区,Product 创建后与其脱离。 |
| Product 交付保证 | 请求对象字段稳定,无法在发送途中被 setter 改成另一套配置。 |
build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。
七、与构造器、JavaBean 和工厂的选择边界
- 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
- 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
- JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
- Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
- 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
- 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
- 链式
return this只是语法,校验、默认值、拷贝和收口才是设计语义。 - 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。
Point(x,y) 这类短而清晰的值对象不需要 Builder;HTTP 请求这类多可选项和组合规则对象才匹配模式动机。
八、交付前的代码与测试检查
- 缺少每个必填字段分别调用
build(),应得到明确且稳定的异常。 - 完全不设置可选字段,核对默认值来自唯一权威位置。
- 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
- 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
- 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
- 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
- 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
- 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
- 若使用 Lombok,检查生成代码、默认值、
@Singular、构造器可见性和框架兼容。 - 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。
记忆钩子:字段多只是信号,真正决定使用 Builder 的是命名步骤、不变量和成品边界。
九、常见误区与追问
- 误区:建造者模式的定义就是链式调用。 链式是常见语法,核心是分离构建过程与最终表示。
- 误区:Builder 一定比构造器更少代码。 它通常增加代码,换取可读性、校验和演进能力。
- 误区:build 可以返回仍需 setter 补全的对象。 这会破坏统一交付合法成品的边界。
- 追问:Builder 适合哪些对象? 字段/可选项多、组合规则复杂、希望不可变或构建步骤需复用的对象。
- 追问:Director 是否属于必选角色? 不是;现代链式 Builder 常由客户端直接组织步骤。
- 追问:Builder 与 Factory 最大区别? Builder 关心同一复杂对象怎样组装,Factory 关心选择哪种产品。
十、加强记忆
建造者模式的记忆锚点是:对象字段少,用构造方法;对象字段多、可选项多、规则多,就把创建过程交给 Builder。它的价值不在于“写起来酷”,而在于让复杂对象的创建更可读、更安全、更集中。