建造者模式

设计模式笔记:建造者模式 (Builder Pattern)

一、一句话概括

将一个复杂对象的构建过程与其最终表示相分离,使得同样的构建过程可以创建出不同配置的对象。通俗地说,就是 “把组装电脑的活儿交给专业的装机师傅,你只需要告诉他要什么配置就行了”

二、为什么需要它 (Why - The Pain Point)

想象一下,当你要创建一个具有很多属性的对象时,会遇到哪些困境:

  1. **伸缩构造函数 (Telescoping Constructor Problem)**:为了应对不同的属性组合,你可能会创建一堆重载的构造函数。

    1
    2
    3
    4
    5
    6
    // 痛点:构造函数爆炸,参数列表又长又乱
    public Pizza(String dough, String sauce) { ... }
    public Pizza(String dough, String sauce, String cheese) { ... }
    public Pizza(String dough, String sauce, String cheese, String topping1) { ... }
    // 调用时像噩梦:
    Pizza myPizza = new Pizza("Thin Crust", "Tomato", "Mozzarella", null, "Pepperoni", null);

    这种代码可读性极差,且非常容易传错参数。

  2. JavaBeans 模式的缺陷:另一种方式是提供一个无参构造函数,然后通过一系列 setter 方法来设置属性。

    1
    2
    3
    4
    5
    6
    // 痛点:对象在构建过程中状态不一致,且无法保证不可变
    Pizza myPizza = new Pizza();
    myPizza.setDough("Thin Crust");
    myPizza.setSauce("Tomato");
    // ... 在调用 build() 之前,myPizza 是一个不完整的“半成品”
    myPizza.setCheese("Mozzarella");

    这种方式有两个主要问题:

    • 状态不一致:在所有 setter 被调用完毕之前,这个 Pizza 对象处于一个不完整、不可用的状态。
    • 无法实现不可变性:由于 setter 方法是公开的,对象在创建后可以被随意修改,这在多线程环境下或需要稳定状态的场景中是危险的。

核心问题:如何创建一个拥有众多参数(尤其是可选参数)的复杂对象,同时保证过程清晰、对象状态一致且最终不可变?

三、它是什么 (What - The Solution)

建造者模式优雅地解决了上述问题。它引入了一个独立的 Builder 对象,专门负责接收参数和构建最终的产品(Product)。

核心思想:

  1. **产品 (Product)**:这是我们想要创建的复杂对象(如 Computer, Pizza)。它的构造函数是私有的,防止外部直接用 new 创建。
  2. 建造者 (Builder):这是一个帮助类,通常作为产品的静态内部类
    • 它拥有与产品完全相同的属性。
    • 它提供一系列流畅的(fluent)设置方法(如 cpu("i9")ram("32G")),每个方法都返回 Builder 自身,以支持链式调用
    • 它有一个最终的 build() 方法,负责用当前收集到的所有属性来创建并返回一个完整的产品实例。
  3. **客户端 (Client)**:客户端通过 Builder 来配置和构建对象,代码清晰明了,且产品一旦通过 build() 创建,就是完整的、不可变的。

C++ 背景对比 & Java 关键点:

  • 与C++的联系:C++ 也可以通过方法链实现类似效果,但 Java 的静态内部类是实现该模式的“黄金搭档”。它将 BuilderProduct 紧密地组织在一起,逻辑上清晰,访问权限也易于控制(Builder 可以访问 Product 的私有构造函数)。
  • Java 的实现精髓
    • 静态内部类 (public static class Builder): 使得你可以直接通过 Product.Builder 来创建建造者,而不需要先有一个 Product 实例。
    • 链式调用 (Method Chaining): builder.foo().bar().baz(),代码如行云流水。
    • 不可变性 (Immutability): Product 的所有字段都设为 final,并且只在私有构造函数中赋值一次,创建后无法更改。

四、怎么做 (How - The Blueprint)

基本步骤(以构建一台电脑为例):

  1. 创建最终产品类 Computer:
    • 将其所有字段声明为 final
    • 创建一个私有的构造函数,它接受一个 Builder 对象作为参数,并用 Builder 的字段来初始化自己的 final 字段。
  2. Computer 内部,创建 public static class Builder:
    • Builder 中,定义与 Computer 完全相同的字段(但非 final)。
    • Builder 的构造函数只接受必选参数。
    • 为所有可选参数创建设置方法(如 gpu(...), motherboard(...))。这些方法必须返回 Builder 自身 (return this;)。
  3. Builder 中,创建 build() 方法:
    • 这个方法调用 Computer 的私有构造函数 new Computer(this),将自己作为参数传入,从而创建并返回一个 Computer 实例。
    • 可以在 build() 方法中添加校验逻辑,确保对象状态的合法性。

Java 示例代码 (经典实现)

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
// 1. 最终产品类 (Product)
public class Computer {
// 推荐设为 final,实现不可变性
private final String cpu; // 必选
private final String ram; // 必选
private final String storage; // 必选
private final String gpu; // 可选
private final String motherboard;// 可选

// 2. 构造函数设为 private,只能由 Builder 调用
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.gpu = builder.gpu;
this.motherboard = builder.motherboard;
}

@Override
public String toString() {
return "Computer Specs: \n" +
" CPU: " + cpu + "\n" +
" RAM: " + ram + "\n" +
" Storage: " + storage + "\n" +
" GPU: " + gpu + "\n" +
" Motherboard: " + motherboard;
}

// 3. 公开的静态内部 Builder 类
public static class Builder {
// 必选参数,在构造函数中 final 声明
private final String cpu;
private final String ram;
private final String storage;

// 可选参数,提供默认值
private String gpu = "Integrated Graphics";
private String motherboard = "Standard ATX";

// Builder 的构造函数只接收必选参数
public Builder(String cpu, String ram, String storage) {
this.cpu = cpu;
this.ram = ram;
this.storage = storage;
}

// 4. 为可选参数提供链式调用方法
public Builder gpu(String gpu) {
this.gpu = gpu;
return this; // 返回自身,实现链式调用
}

public Builder motherboard(String motherboard) {
this.motherboard = motherboard;
return this;
}

// 5. build() 方法创建并返回最终的 Computer 实例
public Computer build() {
// 在这里可以添加校验逻辑,比如 if(cpu == null) throw new IllegalStateException(...)
return new Computer(this);
}
}
}

// 客户端代码
public class BuilderPatternDemo {
public static void main(String[] args) {
// 使用建造者创建一台高端游戏电脑
// 代码清晰易读,每个部分的作用一目了然
Computer gamingPC = new Computer.Builder("Intel Core i9-13900K", "64GB DDR5", "2TB NVMe SSD")
.gpu("NVIDIA GeForce RTX 4090")
.motherboard("ASUS ROG Z790")
.build();

System.out.println("--- Gaming PC Configuration ---");
System.out.println(gamingPC);

System.out.println();

// 创建一台只满足基本要求的办公电脑
Computer officePC = new Computer.Builder("Intel Core i5-13400", "16GB DDR4", "512GB SATA SSD")
.build(); // 可选参数使用默认值

System.out.println("--- Office PC Configuration ---");
System.out.println(officePC);
}
}

五、UML 类图

下面是上述例子的 PlantUML 代码。

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
@startuml
skinparam classAttributeIconSize 0
hide empty members

' 产品类
class Computer {
- final String cpu
- final String ram
- final String storage
- final String gpu
- final String motherboard
' 私有构造函数
- Computer(Builder builder)
}

' 静态内部建造者类
' 使用 namespace 或 package 来表示内部类关系
package Computer {
class Builder {
- final String cpu
- final String ram
- final String storage
- String gpu
- String motherboard
+ Builder(cpu, ram, storage)
+ Builder gpu(String gpu)
+ Builder motherboard(String mb)
+ Computer build()
}
}

' 客户端
class BuilderPatternDemo {
+ {static} void main(String[] args)
}

' 关系
' Computer 包含 Builder 作为内部类
Computer +-- Computer.Builder

' BuilderPatternDemo (Client) 使用 Builder
BuilderPatternDemo ..> Computer.Builder : uses

' Builder 创建 Computer
Computer.Builder ..> Computer : creates

@enduml

图示解读:

  • Computer.BuilderComputer 的一个内部类。
  • BuilderPatternDemo (客户端) 依赖于 Computer.Builder 来配置和创建对象。
  • Computer.Builderbuild() 方法最终会创建出一个 Computer 实例。

六、优缺点分析

优点 (Pros):

  1. 极高的可读性:通过描述性的方法名进行配置,代码意图清晰。new Computer.Builder("i9", "32G").gpu("4090").build()new Computer("i9", "32G", null, "4090") 好一万倍。
  2. 强制对象不可变性:可以轻松地创建出不可变对象,这对于并发编程和系统稳定性至关重要。
  3. 封装构建细节:客户端无需知道对象内部的构建细节和约束。复杂的校验逻辑可以封装在 build() 方法中。
  4. 灵活性和可扩展性:添加新的可选参数非常容易,只需在 Builder 中增加一个字段和一个方法即可,不会影响到现有的客户端代码。

缺点 (Cons):

  1. 代码量增加:需要为每个 Product 类额外编写一个 Builder 类,这导致了字段的重复定义和更多的代码。对于简单的对象(属性少于4个),这有点“杀鸡用牛刀”。
  2. 创建开销:在创建最终产品之前,需要先创建一个 Builder 对象。虽然这点开销在大多数情况下可以忽略不计,但也是一个成本。

七、在 Java 中的应用

建造者模式在现代 Java 开发中非常流行,尤其是在构建配置类、请求对象等方面。

  • **StringBuilderStringBuffer**:这是最经典的例子。你通过 append() 方法(相当于 builder 的配置方法)一步步构建字符串,最后通过 toString() 方法(相当于 build())得到最终结果。
  • **java.util.stream.Stream.Builder**:用于手动构建一个 Stream
  • Lombok 框架的 @Builder 注解:这是一个“代码生成器”,你只需在一个类上添加 @Builder 注解,Lombok 就会在编译时自动为你生成完整的建造者模式代码,极大地减少了样板代码。
  • 各大API客户端:例如 AWS SDK, Google Cloud Client Libraries 等,它们的请求对象(S3PutObjectRequest 等)大量使用建造者模式来配置复杂的API请求参数。