请详细解释建造者模式(Builder)的思路、优缺点和代码示例
建造者模式(Builder)
核心思路
建造者模式解决的是复杂对象的构造问题:当一个对象有很多参数,其中一些必填、一些选填、一些之间还有约束关系时,直接用构造器或一堆 setter 会让调用方很痛苦。
来看问题是怎么出现的。
没有建造者时的两种困境
困境一:伸缩构造器(Telescoping Constructor)
为了应对不同的参数组合,写一堆重载构造器:
public class Pizza {
public Pizza(String size) { ... }
public Pizza(String size, boolean cheese) { ... }
public Pizza(String size, boolean cheese, boolean pepperoni) { ... }
public Pizza(String size, boolean cheese, boolean pepperoni, boolean mushroom) { ... }
// 继续加参数?继续加重载...
}
// 调用方完全看不懂这串 true/false 是什么意思
Pizza p = new Pizza("large", true, false, true);
困境二:JavaBean 风格(大量 setter)
Pizza p = new Pizza();
p.setSize("large");
p.setCheese(true);
p.setPepperoni(false);
// 问题:对象在一堆 setter 调用完之前处于"半构造"状态
// 无法在构造完成时做参数校验,也无法做成不可变对象
两种方式都有缺陷:前者可读性差,后者对象状态不安全。建造者模式同时解决这两个问题。
Java 代码示例
以构建一个 HTTP 请求对象为例,参数多且可选,是建造者的典型场景。
产品类(不可变对象)
public class HttpRequest {
// 必填
private final String method;
private final String url;
// 选填
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
private final boolean followRedirects;
// 构造器私有,只能通过 Builder 创建
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.followRedirects = builder.followRedirects;
}
@Override
public String toString() {
return String.format(
"HttpRequest{\n method=%s\n url=%s\n headers=%s\n" +
" body=%s\n timeout=%dms\n followRedirects=%b\n}",
method, url, headers, body, timeoutMs, followRedirects);
}
// ── Builder 作为静态内部类 ──────────────────────────────
public static class Builder {
// 必填字段
private final String method;
private final String url;
// 选填字段,带默认值
private Map<String, String> headers = new HashMap<>();
private String body = null;
private int timeoutMs = 3000;
private boolean followRedirects = true;
// 必填参数通过构造器传入,保证不可缺少
public Builder(String method, String url) {
if (method == null || url == null)
throw new IllegalArgumentException("method 和 url 不能为空");
this.method = method;
this.url = url;
}
// 每个 setter 返回 this,支持链式调用
public Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeoutMs(int timeoutMs) {
if (timeoutMs <= 0)
throw new IllegalArgumentException("timeout 必须大于 0");
this.timeoutMs = timeoutMs;
return this;
}
public Builder followRedirects(boolean follow) {
this.followRedirects = follow;
return this;
}
// 最终构建,可在此做整体校验
public HttpRequest build() {
if ("POST".equals(method) && body == null)
throw new IllegalStateException("POST 请求必须提供 body");
return new HttpRequest(this);
}
}
}
调用方
public class Main {
public static void main(String[] args) {
// 简单 GET 请求,只填必要参数
HttpRequest get = new HttpRequest.Builder("GET", "https://api.example.com/users")
.header("Authorization", "Bearer token123")
.timeoutMs(5000)
.build();
// 完整 POST 请求
HttpRequest post = new HttpRequest.Builder("POST", "https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"张三\", \"age\": 28}")
.timeoutMs(8000)
.followRedirects(false)
.build();
System.out.println(get);
System.out.println(post);
// 校验生效:POST 不传 body 会抛异常
try {
new HttpRequest.Builder("POST", "https://api.example.com/users").build();
} catch (IllegalStateException e) {
System.out.println("捕获异常: " + e.getMessage());
}
}
}
输出:
HttpRequest{
method=GET
url=https://api.example.com/users
headers={Authorization=Bearer token123}
body=null
timeout=5000ms
followRedirects=true
}
HttpRequest{
method=POST
url=https://api.example.com/users
headers={Content-Type=application/json, Authorization=Bearer token123}
body={"name": "张三", "age": 28}
timeout=8000ms
followRedirects=false
}
捕获异常: POST 请求必须提供 body
建造者的三个关键设计决策
必填参数放构造器,选填参数放链式方法
// 必填:放构造器,缺了连 Builder 都建不出来
new Builder("POST", "https://...")
// 选填:放链式方法,不调就用默认值
.timeoutMs(5000)
.body("...")
这比把所有参数都堆进 build() 要好,IDE 会直接提示你哪些字段必须填。
校验逻辑放 build() 而不是产品类
build() 是构造的最后一步,此时所有参数都已收集完毕,可以做跨字段的整体校验(比如"POST 必须有 body"这种单字段校验做不到的约束)。产品类的构造器只负责赋值,保持简单。
产品类字段全部 final
通过 Builder 构造出来的对象是完整的、不可变的,构造完成后没有任何方式能修改它的状态,天然线程安全,也避免了对象被意外篡改。
优缺点
优点:
- 链式调用可读性强,每个参数都有名字,一眼看清在配置什么
- 必填参数通过构造器强制保证,选填参数有合理默认值
build()时集中校验,构造失败在最早的时间点抛出- 产品对象可以是完全不可变的
缺点:
- 代码量翻倍------每个需要 Builder 的类都要维护一个镜像结构的内部类
- 参数极少(2~3个且全部必填)时完全不必要,直接构造器更简洁
- 如果产品类字段很多,Builder 类本身也会变得很长
适用场景判断
参数超过 4 个、或者有选填参数、或者参数之间有约束关系,就值得引入 Builder。Java 生态里 OkHttpClient、Retrofit、AlertDialog(Android)、ProcessBuilder 都是经典的 Builder 应用。Lombok 的 @Builder 注解可以自动生成这套代码,实际项目里能省不少样板代码。