编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 面向对象设计

  • 常见设计原则

  • 巧学设计模式

    • README
    • 单例模式设计思想
    • 工厂模式设计思想
    • 建造者模式设计思想
      • 01.案例引入与思考
        • 1.1 痛点场景
        • 1.2 它哪里不舒服
        • 1.3 引出本篇主角
      • 02.直觉方案探索
        • 2.1 尝试:重叠构造函数(Telescoping Constructor)
        • 2.2 尝试:无参构造 + setter
        • 2.3 两次失败之后——需求清单收敛
      • 03.建造者模式基础
        • 3.1 从失败中提炼的标准骨架
        • 3.2 建造者模式定义
        • 3.3 典型使用场景
      • 04.建造者模式结构
        • 4.1 四个角色与职责
        • 4.2 时序图
        • 4.3 🧪 ResourcePoolConfig 三步演化
      • 05.盖房子实战对比
        • 5.1 普通方式:流程与产品耦合
        • 5.2 Builder 方式:产品与流程分离
      • 06.用前用后效果对比
        • 6.1 可读性与事故防御
        • 6.2 核心收益
      • 07.反面踩坑与拓展
        • 7.1 🚨 Builder 用错的三种典型事故
        • 7.2 建造者可以简化吗
        • 7.3 和工厂模式区别
      • 08.总结与延伸
        • 8.1 演化逻辑沉淀
        • 8.2 决策树
        • 8.3 真实开源代码中的建造者
        • 8.4 思考题
      • 04.建造者模式分析
        • 4.1 建造者模式结构图
        • 4.2 建造者模式时序图
      • 05.建造者案例实践
        • 5.1 盖房子案例开发
        • 5.2 普通方式盖房子
        • 5.3 构造者优化盖房子
      • 06.建造者模式拓展
        • 6.1 建造者能简化吗
        • 6.2 和工厂模式区别
      • 07.建造者优缺点分析
        • 7.1 优点有哪些
        • 7.2 不足的点分析
      • 08.构造者模式总结
        • 8.1 该模式总结
        • 8.2 模式联动与边界
    • 原型模式设计思想
    • 静态代理设计模式
    • 动态代理设计模式
    • 适配器模式设计思想
    • 装饰者模式设计思想
    • 外观模式设计思想
    • 桥接模式设计思想
    • 组合模式设计思想
    • 享元模式设计思想
    • 观察者模式设计思想
    • 策略者模式设计思想
    • 模版模式设计思想
    • 迭代器模式设计思想
    • 职责链模式设计思想
    • 命令模式设计思想
    • 状态模式设计思想
    • 备忘录模式设计思想
    • 中介者模式设计思想
    • 访问者模式设计思想
    • 解释器模式设计思想
    • 23种设计模式概括
    • 技术写作模板
  • 系统架构设计

  • 编程
  • 巧学设计模式
杨充
2019-09-26
目录

建造者模式设计思想

# 第三卷第3章:建造者模式设计思想

📚 本篇渐进学习节奏(建议按顺序食用)

  1. 第 01 节 · 案例引入 — 一段"12 参构造函数 + setter 漏调用"的线上事故
  2. 第 02 节 · 直觉探索 — 重载构造/setter 两种方案为什么全翻了车
  3. 第 03 节 · 模式基础 — 从失败中提炼标准骨架、定义与场景
  4. 第 04 节 · 模式结构 — 四角色 + 时序图 + 三步演化
  5. 第 05 节 · 盖房子实战 — 普通 vs Builder 方式对比
  6. 第 06 节 · 效果对比 — 改造前后数据说话
  7. 第 07 节 · 踩坑拓展 — Builder 用错的三种事故 + 和工厂区别
  8. 第 08 节 · 总结决策 — 沉淀表 + 决策树 + 开源实例

阅读到任何一节卡壳,直接跳回上一节复盘场景;本篇代码均可直接运行。

# 目录介绍

  • 01.案例引入与思考
    • 1.1 痛点场景
    • 1.2 它哪里不舒服
    • 1.3 引出本篇主角
  • 02.直觉方案探索
    • 2.1 尝试重叠构造函数
    • 2.2 尝试无参构造加setter
    • 2.3 需求清单收敛
  • 03.建造者模式基础
    • 3.1 标准骨架
    • 3.2 建造者模式定义
    • 3.3 典型使用场景
  • 04.建造者模式结构
    • 4.1 四个角色与职责
    • 4.2 时序图
    • 4.3 三步演化
  • 05.盖房子实战对比
    • 5.1 普通方式
    • 5.2 Builder方式
  • 06.用前用后效果对比
    • 6.1 可读性与事故防御
    • 6.2 核心收益
  • 07.反面踩坑与拓展
    • 7.1 Builder用错的三种事故
    • 7.2 建造者可以简化吗
    • 7.3 和工厂模式区别
  • 08.总结与延伸
    • 8.1 演化逻辑沉淀
    • 8.2 决策树
    • 8.3 开源实例
    • 8.4 思考题

# 01.案例引入与思考

本篇主线:日常开发中最常见的"构造参数失控"

# 1.1 痛点场景

🔥 模拟事故复盘 · 周五下午 17:42 上线

团队封装的 HttpClient 已经迭代到 12 个构造参数。新人接手后,老人交接代码时按 "老一套" 写法 new HttpClient(...),复制了一份隔壁项目的初始化代码,把第 6 个参数 true(启用 gzip)和第 7 个参数 3(重试次数)顺序对换。 编译通过、单测通过、灰度通过 — 晚上 22:00 全量后大盘 QPS 雪崩:所有请求带着错误的 Accept-Encoding,被 CDN 一律返回 500。 排查耗时 47 分钟,回滚 11 分钟,事故定级 P2。

事故根因不在新人,而在那个 12 参的构造函数本身就是一个炸弹。

事故现场代码长这样:

HttpClient client = new HttpClient(
    "https://api.com",  // url
    5000,               // 连接超时
    10000,              // 读取超时
    null,               // 代理 host
    0,                  // 代理 port
    true,               // 是否启用 gzip
    3,                  // 重试次数
    "Bearer xxx",       // 认证头
    null,               // 自定义 header
    "UTF-8",            // 编码
    false,              // 是否走 HTTPS 校验
    null                // 拦截器列表
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

调用方根本看不出第 5 个参数 0 是端口、第 6 个 true 是开启 gzip。一旦把 true 和 3 写反——编译通过、运行炸裂,IDE 一行警告都不会给你。

# 1.2 它哪里不舒服

  • ❌ 可读性归零:调用现场只有一串字面量,需要跳到构造函数定义才知道每个参数是干嘛的;
  • ❌ 可选参数地狱:12 个里只有 3 个必填,其余 9 个常常传 null 或 0,语义混乱;
  • ❌ 参数顺序错位:相同类型的参数(多个 String、多个 int)调换位置编译器都不会警告;
  • ❌ 校验放不下:想做"如果启用 HTTPS 校验,就必须提供证书路径"这种跨字段校验,构造函数里一坨 if 写得很丑;
  • ❌ 不可变性破坏:为了规避构造爆炸,常退化成 new HttpClient() + 一堆 setXxx(),对象失去不可变性,多线程立即出问题。

两种"妥协方案"对比一下,全是雷区:

flowchart LR
    Pro[问题: 12 个参数] --> A[妥协 A: 重叠构造函数<br/>5个/8个/12个 多种重载<br/>组合爆炸 + 维护噩梦]
    Pro --> B[妥协 B: 默认构造 + setter<br/>对象可变, 线程不安全<br/>校验时机错位]
    style Pro fill:#fee
    style A fill:#fee
    style B fill:#fee
1
2
3
4
5
6

# 1.3 引出本篇主角

建造者模式(Builder)的核心思想:把"对象的构造过程"从"对象本身"剥离出来,让调用方用链式、命名、分步骤的方式喂参数,最后在 build() 里一次性校验并产出一个不可变对象。

HttpClient client = HttpClient.builder()
    .url("https://api.com")
    .connectTimeout(5000)
    .readTimeout(10000)
    .gzip(true)
    .retry(3)
    .auth("Bearer xxx")
    .build();   // 此处统一校验
1
2
3
4
5
6
7
8

代码瞬间从天书变成了说明文。回到事故现场:如果当初是 Builder 写法,新人把 .gzip(true) 和 .retry(3) 写反 — 编译器立即报错(类型不匹配),事故根本写不出来。

sequenceDiagram
    participant C as 调用方
    participant B as Builder
    participant H as HttpClient
    C->>B: builder()
    C->>B: .url(...).timeout(...) 链式喂参
    Note over B: 内部累积参数,<br/>不直接暴露 HttpClient
    C->>B: build()
    B->>B: 一次性跨字段校验
    B->>H: new HttpClient(完整参数)
    H-->>C: 不可变对象
1
2
3
4
5
6
7
8
9
10
11

本篇会一步步带你看清"为什么需要 Builder、和工厂/setter 的真正区别、它的代价是什么"。


# 02.直觉方案探索

为什么要学这一节:直接给你 Builder 标准答案是很容易的——但建造者不是凭空发明的。它是在"构造参数爆炸"和"setter 不可变"两个死胡同里撞了无数次墙之后才收敛出来的。

# 2.1 尝试:重叠构造函数(Telescoping Constructor)

【新人方案①:多写几个构造函数重载】

回到 01 节那场事故后,第一反应是"我把必填+可选参数分开,写 3 个构造函数":

// 方案 A:3 个构造函数重载,覆盖"仅 url / url+超时 / 全参数"
public HttpClient(String url) { this(url, 5000, 10000, ...); }
public HttpClient(String url, int conn, int read) { ... }
public HttpClient(String url, int conn, int read, boolean gzip, int retry, ...) { ... }
1
2
3
4

🧪 跑一下,看会出什么问题

// 参数少时还行——但 12 个参数里只有 3 个必填,剩下的可选组合 = 2^9 种:
// ❌ 不可能为每种组合写一个构造函数 → 最终只有"最少参数"和"全参数"两个极端版本
// ❌ 调用方被迫传一堆 null/0:"我只要 url 和 gzip,但必须走全参构造器"
new HttpClient("url", 5000, 10000, null, 0, true, 3, null, null, "UTF-8", false, null);
//                                        ^^^^  ^^^  ^^^                      ^^^^  ^^^^
//                                        全是 null 和 0,顺序还不能错
1
2
3
4
5
6

❌ 失败原因:可选参数越多,构造函数重载越趋近"全参 + 写死默认值"。同类型参数(int timeout vs int retry)顺序调换编译器零警告——这就是 01 节那场事故的根因。

💡 反思:我们需要一种按名传参的能力——调用方只填自己需要的字段,不关心顺序。

# 2.2 尝试:无参构造 + setter

【新人方案②:先 new,再 set】

既然构造函数搞不定,那就回到 JavaBean 模式:

// 方案 B:默认构造 + set 方法链
HttpClient client = new HttpClient();
client.setUrl("https://api.com");
client.setConnectTimeout(5000);
client.setGzip(true);
client.setRetry(3);
1
2
3
4
5
6

🧪 跑一下,会发现两个隐藏问题

// 问题 1:对象有"中间状态"——setUrl 之后、setTimeout 之前,client 已经可以被使用
// 问题 2:对象可变——多线程下,线程 A setUrl 到一半,线程 B 已经拿着旧 url 发请求了
client.setUrl("https://api.com");
doSomething(client);  // 💣 client 此时还有 3 个字段没设完
client.setTimeout(5000);  // 💣 晚了,doSomething 已经拿缺字段的对象跑完了
1
2
3
4
5

❌ 失败原因:对象从 new 到最后一个 set 之间有"半成品窗口期"。多线程下这是并发 bug 的温床。此外,跨字段校验无处安放——"如果启用 HTTPS 校验就必须设置证书路径"这个约束,放在哪个 setter 里都不对。

💡 反思:我们既要"按名传参的便利",又要"对象一旦创建就不可变"的安全性。这两个需求在构造函数和 setter 模式下是矛盾的——Builder 正是为了同时满足两者。

# 2.3 两次失败之后——需求清单收敛

必须满足 来自哪一次失败
① 按名传参、不用记顺序 2.1 重载构造函数失败
② 所有字段填完后才产出对象(无中间状态) 2.2 setter 半成品窗口期失败
③ 对象不可变——创建后无法被外部修改 2.2 setter 多线程不安全失败
④ 跨字段校验能在创建前集中执行 1.2 真事故 + 2.2 setter 校验无处放
⑤ 可选参数有合理默认值、不必显式传 null 2.1 被迫传 null 失败

# 03.建造者模式基础

# 3.1 从失败中提炼的标准骨架

上面五条约束翻译成代码,就是 Builder 模式的灵魂骨骼:

public class HttpClient {
    private final String url;           // ① final → 不可变
    private final int connectTimeout;

    private HttpClient(Builder b) {     // ③ 私有构造,只能由 Builder 调
        this.url = b.url;
        this.connectTimeout = b.connectTimeout;
        // ④ 此处可做跨字段校验
    }

    public static Builder builder() { return new Builder(); }

    public static class Builder {
        private String url;
        private int connectTimeout = 5000;  // ⑤ 默认值

        public Builder url(String v)    { this.url = v; return this; }      // ① 链式按名传参
        public Builder connectTimeout(int v) { this.connectTimeout = v; return this; }

        public HttpClient build() {     // ② 一次 build() 产出不可变对象
            if (url == null) throw new IllegalStateException("url is required");
            return new HttpClient(this);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

三句话记住:内嵌 Builder → 链式 setter → build() 产出。差异只在要不要 Director、要不要抽象 Builder——这就是 06 节要讲的变体。

# 3.2 建造者模式定义

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。关键:分步骤构建 + 最终一次性产出不可变对象。

# 3.3 典型使用场景

  • 构造参数 ≥ 5 个、且大部分可选:HttpClient、OkHttpClient、各种 PoolConfig
  • 字段间有依赖关系:maxIdle ≤ maxTotal、启用 HTTPS → 必须提供证书路径
  • 要求对象不可变:发布后不能再被意外修改,多线程安全
  • 构建步骤有序:SQL AST 构建、HTML/XML 文档生成

反面提醒:参数 ≤ 4 个且全必填 → 直接构造函数更清晰。Builder 不是"永远优先"的选择。

# 04.建造者模式结构

# 4.1 四个角色与职责

角色 职责
Product(产品) 最终要构建的复杂对象(不可变)
Builder(抽象建造者) 声明构建步骤的接口
ConcreteBuilder(具体建造者) 实现步骤、持有中间状态、build() 产出
Director(指挥者) 封装构建流程顺序(可选,06 节会讲何时省略)

# 4.2 时序图

sequenceDiagram
    participant C as Client
    participant D as Director
    participant B as ConcreteBuilder
    participant P as Product
    C->>D: construct()
    loop 逐步装配
        D->>B: buildPart()
        B->>P: 设置组件
    end
    C->>B: getResult()
    B-->>C: 完整 Product
1
2
3
4
5
6
7
8
9
10
11
12

# 4.3 🧪 ResourcePoolConfig 三步演化

回到建筑者最经典的载体——资源池配置。从构造器→setter→Builder 三步演化:

// 阶段 1:构造函数——4 个参数还好,但配置项会膨胀
new ResourcePoolConfig("pool", 16, 8, 0);  // 第三个是 maxIdle 还是 minIdle?

// 阶段 2:setter——解决了可读性,但对象可变
ResourcePoolConfig c = new ResourcePoolConfig("pool");
c.setMaxTotal(16); c.setMaxIdle(8);  // 此时 maxTotal=16、maxIdle=8,minIdle 还是默认值 0——跨字段校验无处安放

// 阶段 3:Builder——链式 + 不可变 + build() 一次性校验
ResourcePoolConfig c = new ResourcePoolConfig.Builder()
    .setName("pool")
    .setMaxTotal(16)
    .setMaxIdle(8)
    .setMinIdle(2)
    .build();  // 此处校验 maxIdle ≤ maxTotal、minIdle ≤ maxIdle
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 05.盖房子实战对比

# 5.1 普通方式:流程与产品耦合

// 普通方式:build() 流程和产品定义混在同一个类里
public abstract class AbstractHouse {
    public abstract void buildBasic();
    public abstract void buildWalls();
    public abstract void roofed();
    public void build() {           // 流程写死在抽象类里
        buildBasic();
        buildWalls();
        roofed();
    }
}
// "盖别墅"只能加子类,流程变不了
1
2
3
4
5
6
7
8
9
10
11
12

# 5.2 Builder 方式:产品与流程分离

// Builder 方式:Director 管流程,Builder 管拼装——各自独立变化
HouseDirector director = new HouseDirector(new CommonHouseBuilder());
House commonHouse = director.constructHouse();    // 普通房

director.setHouseBuilder(new HighBuildingBuilder());
House highBuilding = director.constructHouse();   // 高楼——只换了 Builder
1
2
3
4
5
6

📊 对比结论:

维度 普通方式 Builder 方式
产品与流程 耦合:build() 流程混在产品类里 分离:Director 管流程,Builder 管拼装
增加"别墅"类型 新增子类,重写 3 个方法 新增 VillaBuilder,Director 不动
变化顺序(先封顶后砌墙?) 改所有子类的 build() 只改 Director
调用方可读性 new HighBuilding().build() director.constructHouse()
产出是否可变 子类状态可变,不安全 buildHouse() 返回后可设计为不可变

一句话:普通方式在"一件事不变"时代码更短;Builder 在"流程与产品都可能独立变化"时胜出。

# 06.用前用后效果对比

为什么单独留一节做对比:用 01 节的事故做基准,量化 Builder 到底省了什么。

# 6.1 可读性与事故防御

// ❌ 事故现场:12 参构造函数,第 5 个是端口、第 6 个是 gzip
new HttpClient("url", 5000, 10000, null, 0, true, 3, "Bearer xxx", null, "UTF-8", false, null);

// ✅ Builder:每个参数都有名字,顺序无关
HttpClient.builder()
    .url("https://api.com")
    .connectTimeout(5000).readTimeout(10000)
    .gzip(true).retry(3).auth("Bearer xxx")
    .build();   // 如果 gzip(true) 和 retry(3) 写反→编译期类型不匹配,直接报错
1
2
3
4
5
6
7
8
9

📊 事故防御对比:

事故场景 构造函数 Builder
两个 int 参数顺序写反 ❌ 编译通过,运行期炸裂 ✅ 编译期报错(类型不匹配)
忘记传必填 url ❌ 编译通过,运行期 NPE ✅ build() 里检查抛异常
启用 HTTPS 但没设证书 ❌ 运行到握手才报错 ✅ build() 里跨字段校验拦截

# 6.2 核心收益

🔑 核心收益:Builder 把"构造参数爆炸 + setter 不可变"这个矛盾一次性解决——链式按名传参 + build() 统一校验 + 产出不可变对象。三者缺其一,都只是花哨的 setter 集合。 这就是为什么 OkHttp、Spring、Guava、Lombok 不约而同选了这一种写法。

# 07.反面踩坑与拓展

# 7.1 🚨 Builder 用错的三种典型事故

坑 1:忘了调 build(),直接拿 Builder 当 Product 用

HttpClient.Builder b = HttpClient.builder().url("...").retry(3);
b.send();  // ❌ Builder 上根本没有 send()——有人误给 Builder 加了同名方法
1
2

正解:Builder 只暴露 setter + build(),绝不添加业务方法。

坑 2:复用同一个 Builder 实例,造出两个互相影响的对象

HttpClient.Builder b = HttpClient.builder().url("https://a.com");
HttpClient a = b.build();
b.headers().clear();  // 💥 如果 build() 里没拷贝集合,a 的 headers 也被清空了!
1
2
3

正解:build() 里必须深拷贝所有集合字段:new ArrayList<>(b.headers)。

坑 3:跨字段校验漏写,Builder 价值归零

// ❌ build() 只做了字段级非空校验
// 结果:HTTPS 校验=true 但证书路径=null,直到握手才抛 SSLException
1
2

正解:build() 是 Builder 的灵魂——所有跨字段约束(互斥/依赖/范围)必须在这里集中校验。

# 7.2 建造者可以简化吗

当不需要"同一流程、不同产品"时,可以逐步省略角色:

  1. 省略抽象 Builder:只有一个具体的 HttpClient.Builder,不需要 AbstractBuilder 接口
  2. 省略 Director:构建步骤简单,由 Client 直接链式调用 Builder 的 setter
  3. Lombok @Builder:一个注解生成全部样板代码——但跨字段校验仍需要手动写 build() 方法

# 7.3 和工厂模式区别

维度 工厂模式 建造者模式
关注点 造哪个(多种类型选一个) 怎么造(一种类型分步骤构建)
产物 同一父类的不同子类 同一类的不同配置
构建方式 参数驱动一次产出 链式分步 + build() 终局
典型信号 if (type) / switch → 选子类 构造函数 ≥ 5 参 / setter 满天飞

# 08.总结与延伸

# 8.1 演化逻辑沉淀

阶段 学到了什么
01 HttpClient 事故 12 参构造函数 = 定时炸弹——顺序依赖、编译期无保护
02 两次失败 重载构造搞不定可选参数,setter 破坏了不可变性
03 标准骨架 内嵌 Builder → 链式 setter → build() 校验——三句话就够
04 结构与时序 Director 封装流程顺序、Builder 封装拼装细节
05 盖房子对比 流程与产品解耦——改顺序只改 Director、加类型只加 Builder
06 效果对比 事故防御:顺序写错→编译报错、字段忘记→build() 拦截
07 踩坑拓展 忘调 build()、引用泄漏、跨字段校验漏写——Builder 正确打开方式

🔑 一句话核心:

当"参数多 + 大部分可选 + 字段间有约束 + 想要不可变"四个条件同时出现,Builder 才是最佳时机。否则它只是一层多余的脚手架。

# 8.2 决策树

flowchart TD
    Start([我想创建对象]) --> Q1{参数 ≥ 5 个?}
    Q1 -->|否| Direct[直接 new<br/>构造函数即可]
    Q1 -->|是| Q2{大部分参数可选?}
    Q2 -->|否| Direct
    Q2 -->|是| Q3{字段间有依赖校验?}
    Q3 -->|是| Q4{要求对象不可变?}
    Q4 -->|是| Builder[✅ Builder<br/>链式 + build 校验]
    Q4 -->|否| Setter[⚠️ setter 即可<br/>别过度设计]
    Q3 -->|否| Lombok[✅ Lombok @Builder<br/>一个注解搞定]

    style Direct fill:#eee
    style Builder fill:#dfd
    style Setter fill:#ffe6cc
    style Lombok fill:#e6f3ff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 8.3 真实开源代码中的建造者

出处 代码片段 形态
JDK StringBuilder sb.append(a).append(b).toString() 经典 Builder,产出不可变 String
OkHttp Request.Builder .url(...).header(...).build() 链式 Builder,无 Director/无抽象 Builder
Spring UriComponentsBuilder .scheme("https").host("...").build() 链式 + 不可变
Lombok @Builder 一个注解省 50 行样板代码 编译期生成,build() 需手写跨字段校验
Guava ImmutableList.builder() .add(1).add(2).build() 产出真正不可变集合

# 8.4 思考题

  1. Lombok 的 @Builder 能做"跨字段校验"吗?为什么?
  2. StringBuilder / StringBuffer 算不算建造者模式?它和 GoF 定义的 Builder 有什么本质区别?
  3. 链式调用一定要用 Builder 吗?在原对象上直接 return this 实现 Fluent API,它和 Builder 的边界在哪?

上一篇 02.工厂 → 本篇 → 04.原型:当造一个对象太贵、而你又想要"一个差不多但小改的",工厂和建造者都嫌慢,原型上场。

# 04.建造者模式分析

# 4.1 建造者模式结构图

建造者模式包含如下角色:

  1. Builder:抽象建造者
  2. ConcreteBuilder:具体建造者
  3. Director:指挥者
  4. Product:产品角色

为什么需要 Director:调用方不想记住"先 buildBasic、后 buildWalls、最后 roofed"这个顺序,于是抽出 Director 封装"装配流程"。Builder 专注怎么造,Director 专注运行顺序。如果流程固定、不需变体,Director 的价值会减弱(6.1 会提出可以省略)。

# 4.2 建造者模式时序图

建造者模式时序图如下所示:

sequenceDiagram
    participant C as Client
    participant D as Director
    participant B as ConcreteBuilder
    participant P as Product
    C->>B: new ConcreteBuilder()
    C->>D: new Director(builder)
    C->>D: construct()
    loop 逐步动作
        D->>B: buildPartA()
        B->>P: 装配组件 A
        D->>B: buildPartB()
        B->>P: 装配组件 B
        D->>B: buildPartC()
        B->>P: 装配组件 C
    end
    C->>B: getResult()
    B-->>C: 返回完整 Product
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 05.建造者案例实践

# 5.1 盖房子案例开发

例如,让我们考虑如何创建一个House(房屋)对象。

为了建造一个简单的房子,您需要建造四堵墙和一层地板,安装一扇门,安装一对窗户,并建造一座屋顶。但是,如果您想要一个更大、更明亮的房子,带有后院和其他设施(如供暖系统、管道和电气布线)呢?

最简单的解决方案是扩展基类House并创建一组子类来涵盖所有参数的组合。

但是,最终您将得到相当数量的子类。任何新的参数,如门廊风格,都将需要进一步扩展这个层次结构。建造者模式 (opens new window)允许您逐步构建复杂的对象。

# 5.2 普通方式盖房子

使用普通方式盖房子,代码如下所示:

public class BuilderHouse {

    public static void main(String[] args) {
        CommonHouse commonHouse = new CommonHouse();
        commonHouse.build();
        HeightBuilding heightBuilding = new HeightBuilding();
        heightBuilding.build();
    }

    public static abstract class AbstractHouse {

        /**
         * 打地基
         */
        public abstract void buildBasic();

        /**
         * 砌墙
         */
        public abstract void buildWalls();

        /**
         * 封顶
         */
        public abstract void roofed();

        public void build() {
            buildBasic();
            buildWalls();
            roofed();
        }

    }

    public static class CommonHouse extends AbstractHouse {

        @Override
        public void buildBasic() {
            System.out.println(" 普通房子打地基 ");
        }

        @Override
        public void buildWalls() {
            System.out.println(" 普通房子砌墙 ");
        }

        @Override
        public void roofed() {
            System.out.println(" 普通房子封顶 ");
        }

    }

    public static class HeightBuilding extends AbstractHouse {

        @Override
        public void buildBasic() {
            System.out.println(" 高楼打地基 ");
        }

        @Override
        public void buildWalls() {
            System.out.println(" 高楼房子砌墙 ");
        }

        @Override
        public void roofed() {
            System.out.println(" 高楼房子封顶 ");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

分析

  1. 优点:比较好理解,简单易操作
  2. 缺点:程序结构过于简单,没有设计缓存层对象,程序的扩展和维护不好。这种设计方案,把产品(即:房子) 和 创建产品的过程(即:建房子流程) 封装在一起,耦合性增强了
  3. 改进:使用建造者模式,将产品和产品建造过程解耦

# 5.3 构造者优化盖房子

使用构建者模式实现房子的构建

private void test() {
    ///盖普通房子
    //准备创建房子的指挥者
    HouseDirector houseDirector = new HouseDirector(new CommonHouse());
    //完成盖房子,返回产品(普通房子)
    House commonHouse = houseDirector.constructHouse();
    System.out.println("普通房子:" + commonHouse.toString());
    ///盖高楼
    //重置建造者,改成修高楼
    houseDirector.setHouseBuilder(new HighBuilding());
    //完成盖房子,返回产品(高楼)
    House highBuilding = houseDirector.constructHouse();
    System.out.println("高楼:" + highBuilding.toString());
}

/**
 * 产品->Product
 */
public class House {
    private String basic;
    private String wall;
    private String roofed;

    public String getBasic() {
        return basic;
    }

    public void setBasic(String basic) {
        this.basic = basic;
    }

    public String getWall() {
        return wall;
    }

    public void setWall(String wall) {
        this.wall = wall;
    }

    public String getRoofed() {
        return roofed;
    }

    public void setRoofed(String roofed) {
        this.roofed = roofed;
    }

    public House(String basic, String wall, String roofed) {
        this.basic = basic;
        this.wall = wall;
        this.roofed = roofed;
    }

    public House() {
    }

    @Override
    public String toString() {
        return "House{" +
                "basic='" + basic + '\'' +
                ", wall='" + wall + '\'' +
                ", roofed='" + roofed + '\'' +
                '}';
    }
}

/**
 * 抽象的建造者
 */
public abstract class HouseBuilder {
    /**
     * 组合House
     */
    protected House house = new House();

    //-------------------------将建造的流程写好--------------------------

    /**
     * 打地基
     */
    public abstract void buildBasic();

    /**
     * 砌墙
     */
    public abstract void buildWalls();

    /**
     * 封顶
     */
    public abstract void roofed();

    /**
     * 建造好房子后将产品(房子) 返回
     *
     * @return
     */
    public House buildHouse() {
        return house;
    }
}


/**
 * 具体建造者
 */
public class CommonHouse extends HouseBuilder {

    @Override
    public void buildBasic() {
        System.out.println("普通房子打地基5米 ");
        super.house.setBasic("地基5米");
    }

    @Override
    public void buildWalls() {
        System.out.println("普通房子砌墙10cm ");
        super.house.setWall("墙10cm");
    }

    @Override
    public void roofed() {
        System.out.println("普通房子屋顶 ");
        super.house.setRoofed("普通房子屋顶");
    }

}

/**
 * 具体建造者
 */
public class HighBuilding extends HouseBuilder {

    @Override
    public void buildBasic() {
        System.out.println("高楼的打地基100米 ");
        super.house.setBasic("地基100米");
    }

    @Override
    public void buildWalls() {
        System.out.println("高楼的砌墙20cm ");
        super.house.setWall("墙20cm");
    }

    @Override
    public void roofed() {
        System.out.println("高楼的透明屋顶 ");
        super.house.setRoofed("透明屋顶");
    }
}


/**
 * 指挥者,调用制作方法,返回产品
 */
public class HouseDirector {
    /**
     * 聚合
     */
    HouseBuilder houseBuilder = null;

    /**
     * 方式一:构造器传入 houseBuilder
     *
     * @param houseBuilder
     */
    public HouseDirector(HouseBuilder houseBuilder) {
        this.houseBuilder = houseBuilder;
    }

    /**
     * 方式二:通过setter 传入 houseBuilder
     *
     * @param houseBuilder
     */
    public void setHouseBuilder(HouseBuilder houseBuilder) {
        this.houseBuilder = houseBuilder;
    }

    /**
     * 指挥者统一管理建造房子的流程
     *
     * @return
     */
    public House constructHouse() {
        houseBuilder.buildBasic();
        houseBuilder.buildWalls();
        houseBuilder.roofed();
        return houseBuilder.buildHouse();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

🎯 盖房子两种写法的对比

同一个"盖一栋高楼"的需求,两种写法在维护性上的区别:

维度 5.2 普通方式(AbstractHouse 子类) 5.3 Builder 方式
产品与流程 耦合:build() 流程和产品定义混在类里 分离:Director 管流程,Builder 管拼装
增加"别墅"类型 新增一个子类,重写 3 个方法 新增一个 VillaBuilder,不动 Director
变化顺序(先封顶后砌墙?) 要改所有子类的 build() 只改 Director,建造者不动
调用方可读性 new HighBuilding().build() director.constructHouse() 语义更明确
多线程安全 子类状态可变,不安全 产出 Product 不可变,天然安全

一句话:普通方式在"一件事不变"时代码更短;Builder 在"流程与产品都可能变"时胜出。

# 06.建造者模式拓展

# 6.1 建造者能简化吗

为什么要简化:4 个角色全上阵是为了"同一流程、不同实现"。但实际业务中,很多场景只是"参数多、需要链式",背水一整套会显得过重。于是产生了两种逐步变体:

  1. 省略抽象建造者角色:如果系统中只需要一个具体建造者的话,可以省略掉抽象建造者。指挥者直接面向具体建造者;
  2. 省略指挥者角色:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略指挥者角色,让 Builder 角色扮演指挥者与建造者双重角色。上节的 1.3 调用示例 HttpClient.builder().url(...).build() 就是这种"双重却退"后的经典形态,Lombok @Builder 生成的代码也是同一货色。

# 6.2 和工厂模式区别

实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。

建造者模式是用来创建一种类型的复杂对象。通过设置不同的可选参数,"定制化"地创建不同的对象。

网上有一个经典的例子很好地解释了两者的区别。

顾客走进一家餐馆点餐,我们利用工厂模式 (opens new window),根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。

也不要太学院派,非得把工厂模式 (opens new window)、建造者模式 (opens new window)分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题。

# 07.建造者优缺点分析

# 7.1 优点有哪些

建造者优点分析

  1. 在建造者模式中, 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  2. 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象 。
  3. 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  4. 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合"开闭原则"。

# 7.2 不足的点分析

建造者缺点分析

  1. 建造者模式 (opens new window)所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
  2. 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

🚨 反面踩坑实录 · Builder 用错的三种典型姿势

看似无脑的 Builder,真上手后照样能踩进坑里:

坑 1:忘了调 build(),直接拿 Builder 当 Product 用

// 这段代码编译通过,但运行期 NPE
HttpClient.Builder builder = HttpClient.builder().url("...").retry(3);
builder.send();   // ❌ Builder 上没有 send 方法,但同事给 Builder 加了同名方法,触发逻辑分裂
1
2
3

某次重构把 "工具方法" 误加到 Builder 而非 HttpClient 上,导致部分调用方直接在 Builder 上发请求,绕过了 build() 的统一校验。根因:Builder 暴露给外部的方法面太宽,应当只保留 setter + build()。

坑 2:复用同一个 Builder 实例,造出两个"互相影响"的对象

HttpClient.Builder b = HttpClient.builder().url("https://a.com");
HttpClient a = b.build();           // url=a.com
HttpClient c = b.url("https://b.com").build();   // url=b.com — 这没问题
// 但如果 build() 里 new 出的 Product 持有的是 Builder 内部 List 的同一引用...
b.headers().clear();   // 💥 a 的 headers 也被清空了!
1
2
3
4
5

根因:build() 里若直接把 Builder 的可变集合赋给 Product,会泄漏可变状态。正确做法是 new ArrayList<>(builder.headers) 拷贝一份。

坑 3:跨字段校验漏写,Builder 的最大价值反而没用上

public HttpClient build() {
    // ❌ 只做了字段级校验,没做跨字段校验
    return new HttpClient(url, timeout, ...);
}
// 结果上线后发现:启用了 HTTPS 校验(verifySsl=true) 但没设置证书路径(certPath=null) — 
// 直到第一次握手才抛 SSLException,问题被推迟到了运行期。
1
2
3
4
5
6

正解:build() 是 Builder 的灵魂,所有跨字段约束(互斥/依赖/范围)必须在这里集中校验,否则你只是写了个花哨的 setter 集合。

这三个坑都不是 Builder 模式本身的问题,而是"用了 Builder 但没用对"。Builder 的价值 = 链式可读性 + 不可变性 + 一次性校验,三者缺一不可。

# 08.构造者模式总结

# 8.1 该模式总结

Builder模式有几个好处:

  1. Builder的setter函数可以包含安全检查,可以确保构建过程的安全,有效。
  2. Builder的setter函数是具名函数,有意义,相对于构造函数的一长串函数列表,更容易阅读维护。
  3. 可以利用单个Builder对象构建多个对象,Builder的参数可以在创建对象的时候利用setter函数进行调整

当然Builder模式也有缺点:

  1. 更多的代码,需要Builder这样的内部类
  2. 增加了类创建的运行时开销,但是当一个类参数很多的时候,Builder模式带来的好处要远胜于其缺点。

# 8.2 模式联动与边界

flowchart LR
    建造者 -.对比.-> 工厂[工厂模式]
    建造者 -.产出.-> 不可变对象[不可变对象]
    建造者 -.可与单例搭配.-> 单例
    建造者 -.复杂对象的子件.-> 组合[组合模式]
1
2
3
4
5
模式 关系 一句话区别
工厂 易混 工厂关注"造哪个"(多种类型选一个),建造者关注"分步骤造一个复杂的"
原型 替代 当对象"几乎一样、只改个别字段"时,原型 clone 比 Builder 链式重建更便宜
单例 配合 Builder 自身可以做成线程安全的单例,重复使用
组合 协作 建造者常用来构造一棵树(XML/UI/AST),步骤就是"逐节点拼装"

🔍 真实开源代码中的建造者模式

Builder 模式可能是 23 种里你最常用却最不知道的一个 — 几乎所有现代 Java 库都在用:

模式形态 出处 代码片段 它在解决什么
经典 Builder StringBuilder / StringBuffer sb.append(a).append(b).toString() 字符串"分步骤构建",最终 toString() 出不可变 String
链式 Builder OkHttp Request.Builder new Request.Builder().url(...).header(...).build() HTTP 请求 12+ 字段,全部链式喂参
链式 Builder OkHttp OkHttpClient.Builder .connectTimeout(...).addInterceptor(...).build() 客户端配置项几十个,且需不可变
链式 Builder Spring UriComponentsBuilder .scheme("https").host("...").path("...").build() URL 拼装的可读性救星
注解生成 Lombok @Builder 一个注解生成完整 Builder 类 把样板代码彻底干掉
GoF 经典四角色 java.lang.StringBuilder 内部 + JDK 文档生成器 (Doclet) 含 Director 标准教科书形态,但实战较少见
链式 + 不可变 Guava ImmutableList.builder() .add(1).add(2).build() 产出真正不可变集合
链式 + 终结校验 java.util.stream.Stream.Builder .add().add().build() 流式 API 的入口构造

学习建议:本篇看完,强烈推荐打开 OkHttp 的 Request.Builder (opens new window) 源码瞄一眼 — 你会发现它和 GoF 教科书定义有 3 处典型简化(无 Director / 无抽象 Builder / Builder 即 inner class),这就是 6.1 节讲的"双重却退"在工业级代码里的真实形态。

⚠️ 什么时候不该用

  • 参数 ≤ 4 个、且都是必填:构造函数 + 命名参数已经够清晰,Builder 是过度设计;
  • 对象本身就是可变的:Builder 的最大价值是不可变性 + 一次性校验,如果你后面还会到处 setXxx,等于白搭;
  • 构造步骤无序、无依赖:如果字段之间无任何约束,纯命名参数(Kotlin/Python)或 Lombok @Builder 已经足够,不必手写;
  • 调用频次极低:Builder 引入了一个伴生类的成本,一年才造一次的对象,直接 new 配注释更轻量。

一句话:当"参数多 + 大部分可选 + 字段间有约束 + 想要不可变"四个条件同时出现,才是 Builder 的最佳时机。否则它就是一层多余的脚手架。

💭 思考题

  1. Lombok 的 @Builder 帮你省了几十行模板代码,但它能做"跨字段校验"吗?为什么?
  2. StringBuilder / StringBuffer 算不算建造者模式?它和 GoF 定义的 Builder 有什么本质区别?
  3. 链式调用一定要用 Builder 吗?能否在原对象上直接 return this 实现链式(Fluent API),它和 Builder 的边界在哪?

上一篇 02.工厂 → 本篇 → 04.原型:当造一个对象太贵、而你又想要"一个差不多但小改的",工厂和建造者都嫌慢,原型上场。

上次更新: 2026/06/17, 11:43:57
工厂模式设计思想
原型模式设计思想

← 工厂模式设计思想 原型模式设计思想→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式