创建一个对象最常用的方法就是通过 new 关键字创建出一个对象,但是有的时候这种创建对象的方法并不适用。

建造者模式又称为构建者模式或生成器模式,属于创建型模式,它用于组装复杂的实例。它可将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。用户只需要指定需要建造的类型就可以 得到该类型对应的产品实例 , 不关心建造过程细节。

建造者模式有两种写法,一种是 GoF 中提出的传统的建造者模式写法,一种是现在常用的写法。

传统的建造者模式

这里介绍的建造者模式的实现接近于 GoF 的思想。

登场角色:

  • Product(产品):我们需要获得的类。
  • Builder(抽象建造者):定义用于生成实例的接口(API),准备用于生成实例的方法。
  • ConcreteBuilder(具体的建造者):负责实现 Builder 角色接口(API)的类。ConcreteBuilder 还定义了获取最终生成结果(产品)的方法。
  • Director(监工/指挥者):负责使用 Builder 角色的接口(API)来生成实例,它并不依赖于 ConcreteBuilder 角色(它只调用在 Builder 角色中被定义的方法)。注意是具体类。组装的具体过程隐藏在 Director 中。
  • Client(使用者):使用 Builder 模式。

在 GoF 书中没有 Client 和 Product 这两个角色,只是为了方便理解而引入。

建造者代码如下所示:

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
public class Product {
private int param1 = DEFAULT_VALUE;
private int param2 = DEFAULT_VALUE;
private int param3 = DEFAULT_VALUE;
}

// 抽象建造者
public abstract class Builder {
protected Product product = new Product();

// 一些建造的函数
public abstract Builder buildPart1();
public abstract Builder buildPart2();
public abstract Builder buildPart3();
public abstract Product getProduct();
}

//具体建造者(可以有多个)
public static class ConcreteBuilder extends Builder {
/* 具体建造者可以拥有自己的变量 */
@Override
public Builder buildPart1() {/***/}
@Override
public Builder buildPart2() {/***/}
@Override
public Builder buildPart3() {/***/}
@Override
public Product getProduct(){
/* 获取产品前的一些处理 */
return product;
}
}
}

public class Director{
private Builder builder; // Director 并不关心具体使用了哪个 Builder
// 使用构造方法或 set 方法传入具体的 builder
public Director(Builder builder){
this.builder = builder;
}

// 建造工作,保证了一些必选步骤正确执行
public void build(){
/* 省略一些前置处理操作 */
builder.buildPart1();
builder.buildPart2();
builder.buildPart3();
/* 省略一些后处理操作 */
}
}

// 用户调用方式
public class Client{
public static void main(String[] args){
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.build();
Product product = builder.getProduct();
}
}

image.png

Builder 模式的时序图:

sequenceDiagram
    participant C as Client
    participant D as Director
    participant B as ConcreteBuilder
    activate C
    C ->> B : new
    activate B
    deactivate B
    C ->> D : build
    activate D
    D ->> B : buildPart1
    activate B
    B -->> D : 
    deactivate B
	D ->> B : buildPart2
    activate B
    B -->> D : 
    deactivate B
    D ->> B : buildPart3
    activate B
    B -->> D : 
    deactivate B
    D -->> C : 
    deactivate D
    C ->> B : getProduct
    activate B
    B -->> C : 
    deactivate B
    deactivate C

在建造者模式中,Main 不知道 Builder 类,Director 不知道 ConcreteBuilder 类。可替换性就体现在「只有不知道子类才能替换」。

对于上面建造者模式写法,不同文章中的实现也有所不同:

  • 有些写法是使用 Director 获取产品。这种方法让 Director 完全控制构建流程,Builder 只负责部件构造,构建的代码部分实现了复用。用户只需和 Director 交互。
  • 有些写法是像上面文章讲的那样通过 ConcreteBuilder 获取产品。这种方式会呈现一定的构造灵活度。

我们可以不必纠结上面这些区别,直接来看最常用的写法。

建造者模式常用写法——省略 Director 角色

建造者模式的常用用法,是一种混合模式。

假设我们需要定义一个资源池配置类 ResourcePoolConfig。这里的资源池可以简单理解为线程池、连接池、对象池等。包含以下参数:

  • name:资源名称
  • maxTotal:最大总资源数量
  • maxIdle:最大空闲资源数量
  • minIdle:最小空闲资源数量

我们的构造函数或许是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ResourcePoolConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;

private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;

public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
// 一些参数校验逻辑
}
}

不管可选或必选配置项,我们都用构造方法配置这些参数时,就叫做折叠构造函数模式(Telescoping Constructor Pattern)。

1
2
3
4
5
6
7
8
public class ResourcePoolConfig {
//...
public ResourcePoolConfig(String name) {/***/}
public ResourcePoolConfig(String name, Integer maxTotal) {/***/}
public ResourcePoolConfig(String name, Integer maxIdle, Integer minIdle) {/***/}
public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {/***/}
//...
}

我们发现一些问题:

  • ResourcePoolConfig 的必选配置项变为 8 个、10 个时我们的构造函数会越来越长
  • 当出现多个可选配置项时,我们构造函数的重载方法会越来越多。如果一些参数类型几乎都相似,那么参数传递时很容易传混或出错。

对于可选配置项的一个简单的解决方式为使用 setXXX() 函数来给成员变量赋值,以替代冗长繁琐的构造函数。这也是所谓的 Java Bean 模式。

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
public class ResourcePoolConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;

private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;

// name 必填,所以我们把它放在构造函数中
public ResourcePoolConfig(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("name should not be empty.");
}
this.name = name;
}

public void setMaxTotal(int maxTotal) {/***/}

public void setMaxIdle(int maxIdle) {/***/}

public void setMinIdle(int minIdle) {/***/}
//...省略getter方法...
}

但使用 setXXX() 函数还存在一些问题:

  • 必填的配置不能可以使用 setXXX() 函数,因为如果各个必填配置存在互相依赖校验,这种校验逻辑无法实现。所以必选项配置的问题还是没有解决。
  • 不能保证参数传递的先后次序。
  • 一旦必填的配置多了起来,构造参数的列表还是会长起来。
  • 如果我们希望 ResourcePoolConfig 是不可变的对象,那么 setXXX() 就会出现问题。

为解决上述问题,Builder 模式诞生。

代码如下:

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
public class Product{
private int required_param_1 = DEFAULT_VALUE;
private int optional_param_2 = DEFAULT_VALUE;
private int optional_param_3 = DEFAULT_VALUE;
// 私有的构造函数
private Product(Builder builder){
this.required_param_1 = builder.required_param_1;
this.optional_param_2 = builder.optional_param_2;
this.optional_param_3 = builder.optional_param_3;
}
// 专属于特定产品的内部Builder,拥有和Product一样的参数
public static class Builder{
private int required_param_1 = DEFAULT_VALUE;
private int optional_param_2 = DEFAULT_VALUE;
private int optional_param_3 = DEFAULT_VALUE;
// 必要参数使用创建者模式构建
public Builder(int required_param_1){
this.required_param_1 = required_param_1;
}
// 可选参数使用 set 方式设置。返回值返回 Builder 以供链式调用。
public Builder setOptionalParam2(int val){
this.optional_param_2 = val;
return this;
}
public Builder setOptionalParam3(int val){
this.optional_param_3 = val;
return this;
}
public Product build(){
// 一些验证逻辑可以放在此处
return new Product(this);
}
}
// 此处省略 getter 方法
}

使用方式:

1
2
3
4
5
// 链式调用
Product product = new Product.Builder(val1)
.setOptionalParam2(val2)
.setOptionalParam3(val3)
.build();

与经典的 Builder 模式相比,这种模式把 Director 角色给省略了,另外在内部静态 Builder 的构建方法中返回了 this 自身,方便流式调用。

Lombok 中的 @Builder 注解

Lombok 是一个常用的 Java 库,通过添加注解可自动实现建造者模式代码或 Getter、Setter 等。其中,@Builder 注解可以用来实现建造者模式,省略手动编写建造者模式的代码。

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
import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class Person {
private String name;
private int age;
private String address;
private String email;
private String phone;

// Lombok 中的 @Builder 注解会在编译时自动生成一个名为 Person.PersonBuilder 的内部类
/*******************************
public static class Builder {
private String name;
private int age;
private String address;

public Builder() {}

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

public Builder age(int age) {
this.age = age;
return this;
}

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

// ... etc.

public Person build() {
return new Person(this);
}
}
************************************/
}

使用方式:

1
2
3
4
5
6
7
8
Person person = Person.builder()
.name("王五")
.age(28)
.address("上海市浦东新区")
.email("[email protected]")
.phone("13600000000")
.build();
System.out.println(person);

模式的对比与选用

相关的设计模式:

  • Composite 模式:有些情况下 Builder 模式生成的实例构成了 Composite 模式。
  • 门面模式 Facade:
    • 在 Builder 模式中,Director 角色通过组合 Builder 角色中的复杂方法向外部提供可以简单生成实例的接口(API)(相当于示例程序中的 construct 方法)。
    • Facade 模式中的 Facade 角色则是通过组合内部模块向外部提供可以简单调用的接口(API)。

建造者模式与工厂模式的对比

比较:

  • 建造者模式:让建造者类来负责对象的创建工作。
  • 站内文章工厂模式:工厂类负责对象的创建工作。创建不同但相关类型的对象(继承同一父类或接口 Product 的一组子类),由给定参数决定创建哪种类型的对象。Factory 抽象类定义的是一个实例产生的具体流程,子类实现具体的处理。

模式选用:

  • 当创造一个对象需要很多步骤时 , 适合使用建造者模式
  • 当创造一个对象只需要一个简单的方法就可以完成,适合使用工厂模式

建造者模式与抽象工厂模式的对比

Builder 模式和 Abstract Factory 模式都用于生成复杂的实例。

  • Builder 模式则是分阶段地制作复杂实例。
  • Abstract Factory 模式通过调用抽象产品的接口(API)来组装抽象产品,生成具有复杂结构的实例。抽象工厂模式强调的是产品族的创建,即相关的产品一起被创建出来,而建造者模式强调的是一个复杂对象的创建,即它的各个部分逐步被创建出来。

后记

  • 没搞懂的问题,GoF 中的建造者模式的具体应用在哪里?它能解决构造参数中的必选参数和可选参数的问题吗?

参考与归档

本文 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
class Client

class Director{
builder
{method} build
}

abstract class Builder{
product
{method} {abstract} buildPart1
{method} {abstract} buildPart2
{method} {abstract} buildPart3
{method} {abstract} getProduct
}

class ConcreteBuilder{
{method} buildPart1
{method} buildPart2
{method} buildPart3
{method} getProduct
}

class Product{}

Client -> Director : Uses
Director o- Builder
Builder <|-- ConcreteBuilder
Client --> ConcreteBuilder : Uses
Builder *- Product

参考文章