设计模式——建造者模式模式(AI回答)

请详细解释建造者模式(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 生态里 OkHttpClientRetrofitAlertDialog(Android)、ProcessBuilder 都是经典的 Builder 应用。Lombok 的 @Builder 注解可以自动生成这套代码,实际项目里能省不少样板代码。