封面来源:碧蓝航线 蝶海梦花 活动CG

本文参考:

Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)

知乎专栏:秒懂设计模式之建造者模式(Builder pattern)

《Effective Java 中文版(原书第 3 版)》 —— 俞黎敏译

1. 模式的定义与特点

参考链接:建造者模式(Bulider模式)详解

建造者(Builder)模式的定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。

优点

1、封装性好,构建和表示分离。

2、扩展性好,各个具体的建造者相互独立,有利于系统的解耦。

3、客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。

缺点

1、产品的组成部分必须相同,这限制了其使用范围。

2、如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。

建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。

每个字都认识,但是合在一起就不理解了,这到底是个啥? 🤔

不明白没有关系,这段文字可记可不记,当然记住更好,以后和别人解释就有装逼的资格了。🤣

2. 适用场景

设计模式嘛,最重要的就是如何使用和适用场景。

在《Effective Java 中文版(原书第 3 版)》一书中的第 2 条就说到了:遇到多个构造器参数时要考虑使用构造器。

说人话就是:如果一个类的构造方法参数有多个时,要考虑使用构造器。

其实还是有点模糊,再说人话:当一个类的构造函数参数个数超过 4 个,而且某些参数是可选的参数,考虑使用建造者模式。

问题又来了:

1、建造者模式解决了哪些问题?

2、为什么要使用建造者模式?

3、怎么使用建造者模式?

3. 解决的问题

假设我们有这样一个 Person 实体类:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan 2021/2/3
*/
public class Person {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;
}

这个不完整的实体类中有很多属性,但是有些属性在对象实例化时是必填的,比如 firstNamelastNamegender,而其他属性是可选的。

那现在我想要创建一个 Person 对象,这个对象的设置如下:

firstName  	---> 默
lastName 	---> 烦
gender  	---> 1
age 		---> 19
height		---> 178.2

怎么做呢?

3.1 重叠构造器模式

在《Effective Java 中文版(原书第 3 版)》一书中,最开始也是使用的这种方式,就是创建一个包含需要设置的参数的构造方法。那么我需要创建一个这样的构造方法:

1
2
3
4
5
6
7
public Person(String firstName, String lastName, Integer gender, Integer age, Double height) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
this.age = age;
this.height = height;
}

但是这种方式是很愚蠢的: 😑

1、如果我又不想创建一个这样的对象,我想创建一个还包含 job 属性的对象,那么我又要写一个构造方法。同样,如果我想创建一个缺少某个属性的对象,也要再写一个构造方法。

2、使用构造方法时,构造函数的参数类型有很多,在设置时可能会混淆。比如 gender 和 age 都是 Integer 类型的,但是我不小心将 gender 的值与 age 的值设置反了,编译器并不会报错,我们也需要额外的时间去纠错。

总之,采用 重叠构造器模式 来解决并不是一种好的方式。那还有别的方法吗?

3.2 JavaBeans 模式

在《Effective Java 中文版(原书第 3 版)》一书中,重叠构造器模式模式之后也采用了这种方式。

简单来说:先调用一个无参构造方法来创建对象,然后使用属性的 Setter 方法来设置必须的参数和可选的参数。

比如:

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
public Person() {}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public void setGender(Integer gender) {
this.gender = gender;
}

public void setAge(Integer age) {
this.age = age;
}

public void setHeight(Double height) {
this.height = height;
}

public void setWeight(Double weight) {
this.weight = weight;
}

public void setJob(String job) {
this.job = job;
}

但这种方式也有问题:

因为类中的各个属性是分步设置的,在构建过程中对象的状态容易发生变化,造成错误。

《Effective Java 中文版(原书第 3 版)》一书中是这么说的:

遗憾的是, JavaBeans 模式自身有着很严重的缺点 。 因为构造过程被分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态。 类无法仅仅通过检验构造器参数的有效性来保证一致性 。 试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难 。 与此相关的另一点不足在于, JavaBeans 模式使得把类做成不可变的可能性不复存在 ,这就需要程序员付出额外的努力来确保它的线程安全 。

那有没有究极方案呢?

可以使用建造者模式! 💪

4. 建造者模式

4.1 如何使用

对咱们前面定义的 Person 类来说:

1、在 Person 中创建一个静态内部类 Builder,然后将 Person 中的参数都复制到 Builder 类中

2、在 Person 中创建一个 private (protected 也行)的构造函数,参数为 Builder 类型

3、在 Builder 中创建一个 public 的构造函数,参数为 Person 中必填的那些参数

4、在 Builder 中创建设置函数,对 Person 中那些可选参数进行赋值,返回值为 Builder 类型的实例(用于赋值和链式调用)

5、在 Builder 中创建一个 build() 方法(当然叫啥名都可以,作为构建方法),在其中构建 Person 的实例并返回

4.2 代码展示

Person 实体:

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
/**
* @author mofan 2021/2/3
*/
public class Person {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;

private Person(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.gender = builder.gender;
this.age = builder.age;
this.height = builder.height;
this.weight = builder.weight;
this.job = builder.job;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public Integer getGender() {
return gender;
}

public Integer getAge() {
return age;
}

public Double getHeight() {
return height;
}

public static class Builder {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;

public Builder(String firstName, String lastName, Integer gender) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
}

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

public Builder setHeight(Double height) {
this.height = height;
return this;
}

public Builder setWeight(Double weight) {
this.weight = weight;
return this;
}

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

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

测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testBuilder() {
Person person = new Person.Builder("默", "烦", 1)
.setAge(19)
.setHeight(178.2)
.build();
assertNotNull(person);
assertEquals("默", person.getFirstName());
assertEquals("烦", person.getLastName());
assertEquals(1, person.getGender().intValue());
assertEquals(19, person.getAge().intValue());
assertEquals(178.2, person.getHeight(), 0.0);
}

运行测试方法后,测试通过,没有任何报错!

上述这种方式是在 Java 中简化的使用方式,经典的建造者模式与其有所不同,不感兴趣可以跳过第 5 节。😜

5. 经典建造者模式

5.1 角色与结构

建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个角色构成。

建造者(Builder)模式的主要角色如下:

1、产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。

2、抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()

3、具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。

4、指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。

建造者模式结构图:

建造者模式的结构图

5.2 代码展示

第一步,目标 Person2 类:

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
/**
* @author mofan 2021/2/3
*/
public class Person2 {
private String firstName;
private String lastName;
private Integer gender;
private Integer age;
private Double height;
private Double weight;
private String job;

/**
* 含有必填属性的构造方法
*/
public Person2(String firstName, String lastName,
Integer gender) {
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
}

/**
* 可选属性的 Setter 方法
*/
public void setAge(Integer age) {
this.age = age;
}

/**
* 可选属性的 Setter 方法
*/
public void setHeight(Double height) {
this.height = height;
}

/**
* 可选属性的 Setter 方法
*/
public void setWeight(Double weight) {
this.weight = weight;
}

/**
* 可选属性的 Setter 方法
*/
public void setJob(String job) {
this.job = job;
}

// 省略所有属性的 Getter 方法
}

第二步,抽象构建者类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan 2021/2/3
*/
public abstract class Person2Builder {
public abstract void setAge();

public abstract void setHeight();

public abstract void setWeight();

public abstract void setJob();

public abstract Person2 getPerson2();
}

第三步:实体构建者类。我们可以根据需求构建出多个实体构建者类,比如在此只构建一个 Student 类:

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
/**
* @author mofan 2021/2/3
*/
public class Student extends Person2Builder {

private Person2 person2;

public Student(String firstName, String lastName,
Integer gender) {
person2 = new Person2(firstName, lastName, gender);
}

@Override
public void setAge() {
person2.setAge(19);
}

@Override
public void setHeight() {
person2.setHeight(178.2);
}

@Override
public void setWeight() {
person2.setWeight(125.6);
}

@Override
public void setJob() {
person2.setJob(null);
}

@Override
public Person2 getPerson2() {
return person2;
}
}

第四步、指导者类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mofan 2021/2/3
*/
public class Person2Director {
public void makePerson2(Person2Builder builder) {
/*
* 要为目标实体设置那些可选参数就在此设置
* 比如,我只想让我的目标实体具有 age 和 height 两个可选属性值
*/
builder.setAge();
builder.setHeight();
}
}

第五步,测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testPerson2() {
Person2Director director = new Person2Director();
Student student = new Student("默", "烦", 1);
director.makePerson2(student);
Person2 person2 = student.getPerson2();
assertEquals("默", person2.getFirstName());
assertEquals("烦", person2.getLastName());
assertEquals(1, person2.getGender().intValue());
assertEquals(19, person2.getAge().intValue());
assertEquals(178.2, person2.getHeight(), 0.0);
}

运行测试方法后,测试通过,没有任何报错!

思考

相比于经典建造者模式,最初的建造者模式省略了 Director 这个角色,将构建算法交给了 Client 端,其次将 Builder 写到了要构建的产品类里面,最后采用了链式调用。

从上述实现中可以发现 Student 类中已经给属性值写死了,因此我们也常使用建造者模式创建 一个成员变量不可变的对象。所以在最初的建造者模式中,我们常把 Person 的属性设置成 final 的。如:

1
2
3
4
5
6
7
8
9
10
11
public class Person {
private final String firstName;
private final String lastName;
private final Integer gender;
private final Integer age;
private final Double height;
private final Double weight;
private final String job;

// 省略其他代码
}

6. Lombok 中的 @Builder

6.1 基本使用

观察建造者模式后很容易发现:建造者模式似乎编写步骤都差不多,需要编写的代码也差不多。

在 Lombok 中提供了 @Getter@Setter 注解来解决冗杂 Getter 和 Setter 方法,它还提供了 @Builder 注解来一键生成建造者模式的代码。

创建一个新实体 Animal,并使用 @Builder 注解:

1
2
3
4
5
6
7
8
9
10
11
/**
* @author mofan 2021/2/3
*/
@Builder
@Getter
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author mofan 2021/2/3
*/
public class AnimalTest {
@Test
public void testAnimal() {
Animal animal = Animal.builder()
.name("小黑")
.gender(1)
.type("Dog")
.build();
assertEquals("小黑", animal.getName());
assertEquals(1, animal.getGender().intValue());
assertEquals("Dog", animal.getType());
assertNull(animal.getAge());
}
}

运行测试方法后,测试通过,没有任何报错!

查看一下 taeget 目录中的 Animal.class 文件内容:

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
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;

Animal(String name, String type, Integer age, Integer gender) {
this.name = name;
this.type = type;
this.age = age;
this.gender = gender;
}

public static Animal.AnimalBuilder builder() {
return new Animal.AnimalBuilder();
}

public String getName() {
return this.name;
}

public String getType() {
return this.type;
}

public Integer getAge() {
return this.age;
}

public Integer getGender() {
return this.gender;
}

public static class AnimalBuilder {
private String name;
private String type;
private Integer age;
private Integer gender;

AnimalBuilder() {
}

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

public Animal.AnimalBuilder type(String type) {
this.type = type;
return this;
}

public Animal.AnimalBuilder age(Integer age) {
this.age = age;
return this;
}

public Animal.AnimalBuilder gender(Integer gender) {
this.gender = gender;
return this;
}

public Animal build() {
return new Animal(this.name, this.type, this.age, this.gender);
}

public String toString() {
return "Animal.AnimalBuilder(name=" + this.name + ", type=" + this.type + ", age=" + this.age + ", gender=" + this.gender + ")";
}
}
}

可以看到使用 Lombok 的 @Builder 注解后,确实帮我们在编译器生成了建造者模式的结构。

与我们前面自己编写的建造者模式相比,Lombok 生成的代码添加了公共的静态方法 builder(),并且在建造器 AnimalBuilder 的构造方法不再是公共的,而是 default 默认的,只能在同一包内可见。

因此,我们在构建 Animal 实例时不再需要使用关键词 new,而是直接使用静态方法 builder()

6.2 改进与拓展

需要的改进

如果只用 @Builder 注解,可以看到 Lombok 为 Animal 类生成的构造方法是 default 的(不添加权限修饰符,默认为 default 的)。

之所以用构建器模式,是希望用户用构建器提供的方法去创建实例。但 default 的构造方法,可以被同一包中的类调用。所以,我们需要将这构造方法设置为 private 的。这时就需要用到:

1
@AllArgsConstructor(access = AccessLevel.PRIVATE)

最终的 Animal 类:

1
2
3
4
5
6
7
8
9
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;
}

重新编译后,生成的字节码文件的构造方法为:

1
2
3
4
5
6
private Animal(String name, String type, Integer age, Integer gender) {
this.name = name;
this.type = type;
this.age = age;
this.gender = gender;
}

可以进行的拓展

使用 Lombok 的 @Builder 后虽然生成了建造者模式的结构,也可以实现属性值的选择性设置,但是并没做到属性值的必须设置。要想实现也很简单只需要这样设置即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author mofan 2021/2/3
*/
@Builder(builderMethodName = "hiddenBuilder")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Animal {
private final String name;
private final String type;
private final Integer age;
private final Integer gender;

public static AnimalBuilder builder(String type) {
return hiddenBuilder().type(type);
}
}

使用 @Builder 注解的 builderMethodName 属性为自动生成的 builder 方法设置一个新的名字。

然后我们自己编写一个方法,在这个方法中调用自动生成的方法,并设置必填值。

改写下测试方法进行测试:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testAnimal() {
Animal animal = Animal.builder("Dog")
.name("小黑")
.gender(1)
.build();
assertEquals("小黑", animal.getName());
assertEquals(1, animal.getGender().intValue());
assertEquals("Dog", animal.getType());
assertNull(animal.getAge());
}

运行测试方法后,测试通过,没有任何报错!

查看生成的字节码文件,部分内容为:

1
2
3
4
5
6
7
8
9
// 我们自己编写的代码
public static Animal.AnimalBuilder builder(String type) {
return hiddenBuilder().type(type);
}

// 自动生成的代码
public static Animal.AnimalBuilder hiddenBuilder() {
return new Animal.AnimalBuilder();
}

6.3 代码兼容

无参构造与全参构造

如果没有显式声明一个类的构造方法,那么这个类默认会有一个无参构造方法,但如果声明了一个有参构造方法,这个默认的无参构造方法就不存在了,除非显式声明它。

在很多情况下,我们都不会声明有参方法,而是使用默认的无参构造方法,并利用 Getter/Setter 方法对属性进行赋值。

假设一个类在最初设计时并没有使用 @Builder 注解,在后续编码中突然使用了这个注解,由于 @Builder 注解会产生全参构造方法,为了能够更好地兼容旧代码,需要再使用 @NoArgsConstructor 注解为其声明无参构造方法。

这样显式声明无参构造方法后,@Builder 默认产生的全参构造方法就会失效,导致编译报错,因此还需要添加 @AllArgsConstructor(access = AccessLevel.PRIVATE) 注解。

默认值的处理

如果类中的属性存在默认值,这时再使用了 @Builder 注解,那么这些默认值会失效。

这是因为 Lombok 为我们生成的静态内部类中的属性是不会有默认值的。

为了让这些默认值生效,且能够愉快地使用 @Builder 注解,只需要在设置了默认值的字段上使用 @Builder.Default 注解即可。

7. 总结

建造者模式适用的场景

1、当一个类的构造函数参数个数有多个(超过 4 个),而且某些参数是可选的,或者某些参数又是必选的。

2、要创造的对象是一个成员变量不可变的对象。

理解 创建的对象是一个成员变量不可变的对象

首先在经典建造者模式中,在具体建造者(Concrete Builder)中将属性值写死,导致属性值不可变,这里可以体现成员变量不可变。

在最初的建造者模式中,我们 没有采用 JavaBeans 模式的方式给对象的属性赋值,因此实体中没有先关的 Setter 方法,这样就导致创建出对象的成员变量是不可变的。

因此,在使用建造者模式时,我们常把对象的属性设置为 final 的。

如何实现

1、参考第 4 节

2、使用 Lombok 的 @Builder@AllArgsConstructor 注解

注意

如果一个类中有大量属性,但不需要它是成员变量不可变的对象,我们还需要构建器模式吗?

答案是不需要,只需要把代码赋值改成链式的即可,或者使用 Lombok 中的 @Accessors(chain = true) 实现也行。

说句题外话,有些第三方类库或框架不支持实体的链式调用,因此 Lombok 的 @Accessors(chain = true) 注解不要随意使用。