跳转至

建造者模式

从 HTTP 请求配置说起

你需要构建一个 HTTP 请求:URL 和请求方法是必填的,但超时时间、重试次数、请求头、认证信息、代理设置……这十几个参数大多数时候用默认值,偶尔才需要自定义几个。

一个接收 12 个参数的构造函数是灾难:new Request(url, "POST", 5000, 3, null, null, null, headers, null, null, null, null),没人看得懂哪个 null 代表什么。建造者模式的解法是只声明真正关心的参数,其余保持默认:Request.builder(url).method("POST").timeout(5000).build()——清晰、安全、只设置你需要的。

🔍 定义

建造者模式(Builder Pattern)是一种创建型设计模式,它将一个复杂对象的**构建过程**与其**最终表示**分离,使得同样的构建过程可以产生不同的表示。

核心思想:使用一个专门的 Builder 对象分步骤设置参数,最后调用 build() 一次性生成目标对象。这样既保持了对象构建过程的清晰性,又支持创建后的对象不可变(immutable)。

⚠️ 不使用该模式存在的问题

一个 HTTP 请求对象有十几个参数,其中多数是可选的:

BuilderBadExample.java
package com.example.creational.builder;

import java.util.List;

/**
 * 建造者模式 - 反例
 * 问题1:构造器参数过多,调用方不知道每个位置的含义
 * 问题2:使用 JavaBean 设值,对象在构建完成前处于不一致状态
 */
public class BuilderBadExample {
    public static void main(String[] args) {
        // ❌ 方式1:重叠构造器——参数太多,顺序容易搞错
        HttpRequestBad request = new HttpRequestBad(
            "https://api.example.com/users",
            "GET",
            null,
            30,
            3,
            true
        );
        System.out.println(request);

        // ❌ 方式2:JavaBean 风格——对象构建中途可被使用,状态不一致
        HttpRequestBad javaBeanStyle = new HttpRequestBad();
        javaBeanStyle.setUrl("https://api.example.com/users");
        // 忘记调用 setMethod,对象已经可以被传递出去了 ❌
        System.out.println(javaBeanStyle);
    }
}

class HttpRequestBad {
    private String  url;
    private String  method;
    private String  body;
    private int     timeout;
    private int     retryCount;
    private boolean followRedirects;

    // ❌ 重叠构造器:参数太多,极易传错位置
    public HttpRequestBad(String url, String method, String body,
                          int timeout, int retryCount, boolean followRedirects) {
        this.url             = url;
        this.method          = method;
        this.body            = body;
        this.timeout         = timeout;
        this.retryCount      = retryCount;
        this.followRedirects = followRedirects;
    }

    // ❌ JavaBean 默认构造器:允许创建"半成品"对象
    public HttpRequestBad() {}

    public void setUrl(String url)                      { this.url             = url;             }
    public void setMethod(String method)                { this.method          = method;          }
    public void setTimeout(int timeout)                 { this.timeout         = timeout;         }
    public void setRetryCount(int retryCount)           { this.retryCount      = retryCount;      }
    public void setFollowRedirects(boolean f)           { this.followRedirects = f;               }

    @Override
    public String toString() {
        return "HttpRequest{url=" + url + ", method=" + method
             + ", timeout=" + timeout + ", retry=" + retryCount + "}";
    }
}

两种方式都存在问题:构造函数可读性差,JavaBean 风格无法创建不可变对象且有线程安全隐患。

🏗️ 设计模式结构说明

%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
    classDef default fill:transparent,stroke:#768390
    class HttpRequest {
        -url: String
        -method: String
        -timeout: int
        -headers: Map
        -followRedirects: boolean
        -HttpRequest(Builder)
        +getUrl() String
        +getMethod() String
        +getTimeout() int
    }
    class Builder {
        -url: String
        -method: String
        -timeout: int
        -headers: Map
        -followRedirects: boolean
        +url(url) Builder
        +method(method) Builder
        +timeout(ms) Builder
        +header(k,v) Builder
        +followRedirects(b) Builder
        +build() HttpRequest
    }
    HttpRequest *-- Builder : 内部类
    Builder ..> HttpRequest : 创建
    note for HttpRequest "产品(Product)"
    note for Builder "建造者(Builder)"

核心角色:

角色 说明
Product(产品) 最终被构建的复杂对象,通常是不可变的
Builder(建造者) 提供链式设置参数的方法,持有构建参数
Director(指导者,可选) 封装特定的构建流程,复用常见配置组合

💻 设计模式举例说明

以邮件消息为例,展示带 Director(邮件模板工厂)的完整建造者实现:

BuilderExample.java
package com.example.creational.builder;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 建造者模式 - 正例
 * 内部 Builder 类负责组装,build() 时做校验,构造出不可变的完整对象
 */
public class BuilderExample {
    public static void main(String[] args) {
        // ✅ 链式调用,每个参数含义清晰,且 build() 保证校验
        EmailMessage email = new EmailMessage.Builder("hello@example.com", "重置密码通知")
                .from("noreply@example.com")
                .cc(Arrays.asList("admin@example.com", "log@example.com"))
                .body("<p>请在24小时内完成密码重置</p>")
                .htmlEnabled(true)
                .priority(1)
                .build();

        System.out.println(email);

        // 使用 Director 封装常用构建流程
        EmailMessage welcome = EmailDirector.buildWelcomeEmail("zhangsan@example.com", "张三");
        System.out.println(welcome);
    }
}

// 产品类:不可变的邮件消息
class EmailMessage {
    private final String       to;
    private final String       subject;
    private final String       from;
    private final List<String> cc;
    private final String       body;
    private final boolean      htmlEnabled;
    private final int          priority;

    // 私有构造器:只能通过 Builder 创建
    private EmailMessage(Builder builder) {
        this.to          = builder.to;
        this.subject     = builder.subject;
        this.from        = builder.from;
        this.cc          = Collections.unmodifiableList(builder.cc);
        this.body        = builder.body;
        this.htmlEnabled = builder.htmlEnabled;
        this.priority    = builder.priority;
    }

    @Override
    public String toString() {
        return "EmailMessage{to=" + to + ", subject=" + subject
             + ", from=" + from + ", cc=" + cc
             + ", html=" + htmlEnabled + ", priority=" + priority + "}";
    }

    // ✅ Builder:内部类,链式设置参数
    public static class Builder {
        // 必填参数
        private final String to;
        private final String subject;
        // 可选参数(带默认值)
        private String       from        = "no-reply@example.com";
        private List<String> cc          = new ArrayList<>();
        private String       body        = "";
        private boolean      htmlEnabled = false;
        private int          priority    = 3;

        public Builder(String to, String subject) {
            if (to == null || to.isBlank())      throw new IllegalArgumentException("收件人不能为空");
            if (subject == null || subject.isBlank()) throw new IllegalArgumentException("主题不能为空");
            this.to      = to;
            this.subject = subject;
        }

        public Builder from(String from)            { this.from        = from;        return this; }
        public Builder cc(List<String> cc)          { this.cc          = cc;          return this; }
        public Builder body(String body)            { this.body        = body;        return this; }
        public Builder htmlEnabled(boolean html)    { this.htmlEnabled = html;        return this; }
        public Builder priority(int priority)       { this.priority    = priority;    return this; }

        public EmailMessage build() {
            // 构建时做业务校验,保证产品完整性
            if (htmlEnabled && (body == null || body.isBlank())) {
                throw new IllegalStateException("HTML 邮件必须提供正文");
            }
            return new EmailMessage(this);
        }
    }
}

// Director:封装常用构建配方,进一步简化调用方
class EmailDirector {
    public static EmailMessage buildWelcomeEmail(String toEmail, String userName) {
        return new EmailMessage.Builder(toEmail, "欢迎加入 " + userName + "!")
                .from("welcome@example.com")
                .body("<h1>欢迎 " + userName + "</h1>")
                .htmlEnabled(true)
                .priority(2)
                .build();
    }
}

⚖️ 优缺点

优点

  • 🎯 可读性极佳:链式调用,每个参数都有名字,代码自描述(builder.method("POST").timeout(5000)
  • 🎯 支持不可变对象build() 之后对象完全构建完毕,提供只读访问,线程安全
  • 🎯 参数灵活:可选参数可以随意省略,且带有合理默认值
  • 🎯 可复用构建过程:Director 可以封装常用配置组合,避免重复代码

缺点

  • ⚠️ 代码量增加:Builder 类几乎是 Product 类的镜像,参数越多 Builder 越臃肿
  • ⚠️ 与 Product 强耦合:Product 字段变化时,Builder 也必须同步修改
  • ⚠️ 对简单对象过度设计:参数少于 4 个且都是必填的对象,直接用构造函数更简洁

🔗 与其它模式的关系

相关模式 关系说明
抽象工厂模式 抽象工厂一次性返回完整产品族;建造者关注分步骤构建单个复杂对象,最后调用 build() 返回
模板方法模式 Director 中封装的构建流程本质上是一种模板方法——骨架固定,具体步骤委托给 Builder
组合模式 构建树形结构(如 AST、DOM)时,常用建造者分步组装节点
原型模式 当需要创建"基于某个已有对象的变体"时,原型(clone + 修改)有时比重新用 Builder 构建更简洁

🗂️ 应用场景

  • 🗂️ 复杂配置对象HttpRequestOkHttpClientSSLContext——参数多且部分可选
  • 🗂️ 不可变值对象:需要线程安全且创建后不可修改的对象
  • 🗂️ SQL 查询构建器QueryBuilder.select("*").from("user").where("id > 10").limit(20).build()
  • 🗂️ Lombok @Builder:注解自动生成 Builder,避免手写样板代码
  • 🗂️ Spring Securityhttp.authorizeHttpRequests().requestMatchers(...).permitAll().and()...
  • 🗂️ JDK 内置StringBuilder(可变字符串构建)、ProcessBuilder(进程构建)

Lombok @Builder 使用

实际项目中很少手写 Builder,通常使用 Lombok:

@Builder
@Getter
public class HttpRequest {
    private final String url;
    @Builder.Default private final String method = "GET";
    @Builder.Default private final int    timeout = 3000;
}

// 使用
HttpRequest req = HttpRequest.builder()
        .url("https://api.example.com")
        .method("POST")
        .build();

🏭 工业视角

三种对象创建方式的适用边界

方式 适用场景 主要问题
构造函数 参数少(≤4个),全部必填 参数多时可读性差,易传错顺序
set() 方法 参数多,大部分可选,允许对象可变 无法创建不可变对象;必填项校验无处安放;对象可能处于中间无效状态
Builder 参数多、有必填/可选之分、参数间有约束、需要不可变对象 代码量略多,需多写一个 Builder 内部类

Builder 模式真正解决的三个问题: 1. 必填项校验集中:所有校验逻辑在 build() 方法中统一执行,避免遗漏 2. 参数间约束验证:如 maxIdle <= maxTotal 这类跨字段约束,set 方法无法优雅处理 3. 不可变对象:构造完成后目标类不暴露任何 setter,线程安全

Builder 模式:校验集中,创建不可变对象
// build() 中统一做必填项 + 约束条件校验
public ResourcePoolConfig build() {
    if (StringUtils.isBlank(name)) {
        throw new IllegalArgumentException("name 为必填项");
    }
    if (maxIdle > maxTotal) {
        throw new IllegalArgumentException("maxIdle 不能大于 maxTotal");
    }
    if (minIdle > maxIdle) {
        throw new IllegalArgumentException("minIdle 不能大于 maxIdle");
    }
    return new ResourcePoolConfig(this); // 目标类构造函数私有
}

Builder 与工厂模式的区别

两者都是创建对象,但关注点不同:

  • 工厂模式:关注"创建**哪种**对象"——根据类型参数返回不同子类实例,调用方不关心具体类
  • Builder 模式:关注"如何**配置**同一种对象"——创建的是同一个类,但参数组合复杂

Lombok 的 @Builder

在 Java 项目中,Lombok 的 @Builder 注解可以自动生成 Builder 内部类,省去大量样板代码。 但注意:Lombok 生成的 Builder 不会自动添加必填项校验和约束检查,如有需要仍需手写 build() 方法中的逻辑。