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

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

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

1. 模式的定义与特点

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

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

优点

  1. 封装性好,构建和表示分离
  2. 扩展性好,各个具体的建造者相互独立,有利于系统的解耦
  3. 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。

缺点

  1. 产品的组成部分必须相同,这限制了其使用范围
  2. 如果产品的内部结构复杂,当产品内部发生变化时,建造者也要同步修改,后期维护成本较大
  3. 为了创建对象,必须先创建它的构建器,在十分注重性能的情况下,这是一笔性能开销

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

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

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

2. 适用场景

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

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

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

其实还是有点模糊,再说人话:当一个类的构造函数参数个数超过 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;
}

在创建 Person 对象时,有些属性是必填的,比如 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;
}

但这种方式是很愚蠢的 😑

  • 如果需求发生变更,想创建一个还包含 job 属性的对象,则又要提供一个包含 job 参数的构造函数;同样,如果想创建一个缺少某个属性的对象,也要再提供一个构造函数;

  • 使用构造函数时,构造函数的参数有很多,但有很多相同的类型,在设置时可能会混淆。比如 genderage 都是 Integer 类型的,如果不小心将它们的值设置反了,这个错误不会在编译器体现,后续需要额外的时间去纠错。

总之,采用 重叠构造器模式 来解决并不是一种好的方式。

3.2 JavaBeans 模式

在《Effective Java 中文版(原书第 3 版)》一书中,紧接着又提到了 JavaBeans 模式

简单来说:先调用一个无参构造函数来创建对象,然后使用 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 中提供一个非 public 的构造函数,参数类型为 Builder
  3. Builder 中提供一个 public 的构造函数,参数为构造 Person 对象时的必填参数
  4. Builder 中提供多个设置方法,用于对 Person 中可选参数进行赋值,返回值为 Builder 类型的实例(用于链式调用)
  5. Builder 中提供一个 build() 方法(当然叫啥名都可以,作为构建方法),用于构建最终的 Person 实例并返回

4.2 代码展示

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;

// --snip--
}

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());
}
}

查看 IDEA 反编译 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(),并且 AnimalBuilder 的构造函数不再是 public,仅仅是包私有的。

这样在构建 Animal 对象时不再需要使用关键词 new,而是直接使用静态方法 builder()

6.2 进一步改进

如果只用 @Builder 注解,可以看到 Lombok 为 Animal 类生成的构造函数是包私有的。

之所以用建造者模式,是希望用户 只用 建造者模式提供的方法去创建实例,包私有的构造函数,可以被同一包中的类调用,所以需要将这个构造函数设置为 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;
}

6.3 可以进行的拓展

使用 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.4 代码兼容

无参构造与全参构造

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

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

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

默认值的处理

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

这是因为 Lombok 生成的静态嵌套类中的属性不包含默认值。

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

7. 在类层次结构中使用

建造者模式也适用于类层次结构。抽象类有抽象的 builder,具体类有具体的 builder。

以下代码来自《Effective Java 中文版(原书第 3 版)》 一书。

声明枚举 Topping,表示披萨的各种馅料:

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
/**
* @author mofan
* @date 2022/10/5 12:34
*/
public enum Topping {
/**
* 火腿
*/
HAM,

/**
* 蘑菇
*/
MUSHROOM,

/**
* 洋葱
*/
ONION,

/**
* 胡椒
*/
PEPPER,

/**
* 香肠
*/
SAUSAGE
}

用一个抽象类表示各种各样的披萨:

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
/**
* @author mofan
* @date 2022/10/5 12:34
*/
@Getter
public abstract class Pizza {
final Set<Topping> toppings;

abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}

public abstract Pizza build();

protected abstract T self();
}

public Pizza(Builder<?> builder) {
this.toppings = builder.toppings.clone();
}
}

现有两种具体的披萨,分别是经典纽约风味的披萨和馅料内置的半月型披萨。声明枚举 Size,表示披萨的尺寸:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author mofan
* @date 2022/10/5 13:21
*/
public enum Size {
/**
* 小
*/
SMALL,

/**
* 中
*/
MEDIUM,

/**
* 大
*/
LARGE
}

经典纽约风味的披萨的实现:

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
/**
* @author mofan
* @date 2022/10/5 13:22
* <p>
* 经典纽约风味披萨
*/
@Getter
public class NyPizza extends Pizza {

private final Size size;

public static class Builder extends Pizza.Builder<Builder> {
private final Size size;

public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}

@Override
public NyPizza build() {
return new NyPizza(this);
}

@Override
protected Builder self() {
return this;
}
}

public NyPizza(Builder builder) {
super(builder);
this.size = builder.size;
}
}

馅料内置的半月型披萨的实现:

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
* @date 2022/10/5 14:06
*
* 馅料内置的半月型披萨
*/
@Getter
public class Calzone extends Pizza {
/**
* 是否有酱汁
*/
private final boolean sauceInside;

public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;

public Builder sauceInside() {
this.sauceInside = true;
return this;
}

@Override
public Calzone build() {
return new Calzone(this);
}

@Override
protected Builder self() {
return this;
}
}

public Calzone(Builder builder) {
super(builder);
this.sauceInside = builder.sauceInside;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testPizza() {
NyPizza pizza = new NyPizza.Builder(Size.SMALL)
.addTopping(Topping.SAUSAGE)
.addTopping(Topping.ONION)
.build();
assertEquals(Size.SMALL, pizza.getSize());
assertTrue(pizza.getToppings().containsAll(new HashSet<>(Arrays.asList(Topping.SAUSAGE, Topping.ONION))));

Calzone calzone = new Calzone.Builder()
.addTopping(Topping.HAM)
.sauceInside()
.build();
assertTrue(calzone.isSauceInside());
assertEquals(1, calzone.getToppings().size());
assertTrue(calzone.getToppings().contains(Topping.HAM));
}

8. 总结

建造者模式适用的场景

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

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

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

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

在最初的建造者模式中,没有采用 JavaBeans 模式的方式给对象的属性赋值,因此类中没有提供任何字段的 Setter 方法,创建出来的对象的字段值无法再被修改。

因此,在使用建造者模式时,常用 final 修饰字段。

如何实现

  1. 参考第 4 节
  2. 使用 Lombok 的 @Builder@AllArgsConstructor 注解

如果一个类中有大量成员变量,但它们并不是不可变的对象,还可以建造者模式吗?

这没有标准答案,因为既可以把对字段的赋值过程改成链式的,也可以使用 Lombok 中的 @Accessors(chain = true) 实现链式调用,当然使用建造者模式也没问题。

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

简而言之:如果创建一个对象所用的构造器(Constructor)或静态工厂有多个参数时,可以考虑使用建造者模式;如果这些参数有部分必选的,还有部分是可选的,那就更应该考虑建造者模式。

9. 补充:链式调用新思路

为实现链式调用,常常需要使 Setter 返回 this,或者使用 Lombok 的 @Accessors(chain = true) 注解。两种方式实现的链式调用在编译后生成的 class 文件中的 Setter 都返回了 this,但这在某些情况会出现问题,比如使用 CGLib 的 BeanCopier 进行 Bean 拷贝时会因为 Setter 返回了 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
26
27
28
29
30
31
32
/**
* 链式调用的新思路
* 我并不认为这是建造者模式的通用写法
*
* @author mofan
* @date 2022/11/24 22:08
*/
public class Builder<T> {
private final Supplier<T> CONSTRUCTOR;
private Consumer<T> injector = (t) -> { };

private Builder(Supplier<T> constructor) {
this.CONSTRUCTOR = constructor;
}

public static <T> Builder<T> builder(Supplier<T> constructor) {
return new Builder<>(constructor);
}

public <F> Builder<T> with(BiConsumer<T, F> biConsumer, F f) {
Consumer<T> c = instance -> biConsumer.accept(instance, f);
injector = injector.andThen(c);
return this;
}

public T build() {
T t = CONSTRUCTOR.get();
injector.accept(t);
return t;
}

}

测试一下:

1
2
3
4
5
6
7
8
9
@Test
public void test() {
Student student = Builder.builder(Student::new)
.with(Student::setName, "mofan")
.with(Student::setAge, 20)
.build();
Assert.assertEquals("mofan", student.getName());
Assert.assertEquals(20, student.getAge());
}

有人管这个 Builder 叫做通用 Builder,试图将其与建造者模式联系上,但我认为这个 Builder 与建造者模式没有什么关系。

首先没法做到在创建对象时让一些参数必填,一些参数可选。或许可以提供多个 Builder#builder() 方法的重载?

但这样需要目标类中提供公有的有参构造函数,除此之外,在后续的操作中还要求目标类提供共有的 Setter,让创建出的对象能被随意修改。无论是公有的有参构造函数,还是公用的 Setter,这些在建造者模式中都不应该向外部暴露。

并不是说这个 Builder 一无是处,正如标题写的那样,这为链式调用提供了一种新思路。

10. 补充:Step Builder

参考链接:Step builder pattern

可以认为 Step Builder 是普通 Builder 的升级版,它用于逐步构建复杂对象,提供流式接口来创建具有大量可配置参数的对象。

Step Builder 不仅具有建造者模式的优点,对象的构建过程也更加清晰, 用户只会看到下一步可用的方法, 直到构建对象的正确时间才能看到构建方法。

Step Builder 并不是完美的,实现其功能的代码可读性较低,并且没有类似 Lombok 的第三方库来帮助代码生成。

10.1 需求场景

现有一 Email 类:

1
2
3
4
5
6
7
8
9
@Getter
public class Email {
private final EmailAddress from;
private final List<EmailAddress> to;
private final List<EmailAddress> cc;
private final List<EmailAddress> bcc;
private final String subject;
private final String content;
}

在构造 Email 时,希望能按照一定的顺序完成,比如第一步只能设置 from 的值,第二步只能设置 to 的值。

构造 Email 的过程如下,紫色表示必填值,黄色表示可选值:

step-builder

10.2 代码实现

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
/**
* @author mofan
* @date 2024/1/12 17:58
* @link <a href="http://www.svlada.com/step-builder-pattern/">Step builder pattern</a>
*/
@Getter
public class Email {
private final EmailAddress from;
private final List<EmailAddress> to;
private final List<EmailAddress> cc;
private final List<EmailAddress> bcc;
private final String subject;
private final String content;

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

public sealed interface FromStep permits Builder {
ToStep from(EmailAddress from);
}

public sealed interface ToStep permits Builder {
SubjectStep to(List<EmailAddress> to);
}

public sealed interface SubjectStep permits Builder {
ContentStep subject(String subject);
}

public sealed interface ContentStep permits Builder {
Build content(String content);
}

public sealed interface Build permits Builder {
/**
* 构造 Email 对象
*/
Email build();

/**
* cc 是可选的
*/
Build cc(List<EmailAddress> cc);

/**
* bcc 也是可选的
*/
Build bcc(List<EmailAddress> bcc);
}

private Email(Builder builder) {
this.from = builder.from;
this.to = builder.to;
this.cc = builder.cc;
this.bcc = builder.bcc;
this.subject = builder.subject;
this.content = builder.content;
}

public final static class Builder implements FromStep, ToStep, SubjectStep, ContentStep, Build {
private EmailAddress from;
private List<EmailAddress> to;
private String subject;
private String content;
private final List<EmailAddress> cc = new ArrayList<>();
private final List<EmailAddress> bcc = new ArrayList<>();

@Override
public Email build() {
return new Email(this);
}

@Override
public ToStep from(EmailAddress from) {
Objects.requireNonNull(from);
this.from = from;
return this;
}

@Override
public SubjectStep to(List<EmailAddress> to) {
if (CollectionUtils.isEmpty(to)) {
throw new RuntimeException();
}
this.to = new ArrayList<>(to);
return this;
}

@Override
public ContentStep subject(String subject) {
Objects.requireNonNull(subject);
this.subject = subject;
return this;
}

@Override
public Build content(String content) {
Objects.requireNonNull(content);
this.content = content;
return this;
}

@Override
public Build cc(List<EmailAddress> cc) {
this.cc.addAll(CollectionUtils.emptyIfNull(cc));
return this;
}

@Override
public Build bcc(List<EmailAddress> bcc) {
this.bcc.addAll(CollectionUtils.emptyIfNull(bcc));
return 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
@Test
public void testBuild() {
Email email = Email.builder()
.from(EmailAddress.from("A"))
.to(List.of(EmailAddress.from("B"), EmailAddress.from("C")))
.subject("subject")
.content("content")
.bcc(List.of(EmailAddress.from("D")))
.cc(List.of(EmailAddress.from("E")))
.build();

assertThat(email.getFrom().getAddress()).isEqualTo("A");
assertThat(email.getTo())
.map(EmailAddress::getAddress)
.containsExactly("B", "C");
assertThat(email.getSubject()).isEqualTo("subject");
assertThat(email.getContent()).isEqualTo("content");
assertThat(email.getBcc()).map(EmailAddress::getAddress)
.singleElement()
.isEqualTo("D");
assertThat(email.getCc()).map(EmailAddress::getAddress)
.singleElement()
.isEqualTo("E");
}

10.3 柯里化

参考链接:Currying

百度百科对 柯里化 的解释是:在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

给定函数 f : (X×Y)Zf \ : \ (X \times Y) \rightarrow Z,使用科里化得到新函数 h : X(YZ)h \ : \ X \rightarrow (Y \rightarrow Z)hhXX 中获取一个参数,然后返回一个将 YY 映射到 ZZ 的函数,因此有 h(x)(y)=f(x,y)h(x)(y)=f(x,y)

以 JavaScript 为例,存在名为 add 的函数,接收三个参数,最终返回它们的和:

1
2
3
4
5
function add(a, b, c) {
return a + b + c
}

add(1, 2, 3) // 6

如果对 add 函数进行柯里化,那么有:

1
2
3
4
5
6
7
8
9
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}

curriedAdd(1)(2)(3) // 6

在 Java8 引入 Lambda 表达式后,Java 也能很轻松实现类似的功能。

比如现在需要构造一个 Book 对象,需要依次传入类型、作者、书名和出版时间等信息。一般来说,一个作者会出版许多同类型、不同名称的书籍,在构造这样的 Book 对象时,如果每次都传入相同的类型和作者信息会使得代码很冗余,为此可以参考柯里化编写出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum Genre {
/**
* 奇幻
*/
FANTASY,

/**
* 恐怖
*/
HORROR,

/**
* 科幻
*/
SCI_FI,
}
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
public record Book(Genre genre, String author, String title, LocalDate publicationDate) {
public static AddGenre builder() {
return genre
-> author
-> title
-> publicationDate
-> new Book(genre, author, title, publicationDate);
}

public interface AddGenre {
Book.AddAuthor withGenre(Genre genre);
}

public interface AddAuthor {
Book.AddTitle withAuthor(String author);
}

public interface AddTitle {
Book.AddPublicationDate withTitle(String title);
}

public interface AddPublicationDate {
Book withPublicationDate(LocalDate publicationDate);
}

}

现在可以这样构造 Book 对象:

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
@Test
public void test() {
// Defining genre book functions
Book.AddAuthor fantasyBookFunc = Book.builder().withGenre(Genre.FANTASY);
Book.AddAuthor horrorBookFunc = Book.builder().withGenre(Genre.HORROR);
Book.AddAuthor scifiBookFunc = Book.builder().withGenre(Genre.SCI_FI);

// Defining author book functions
Book.AddTitle kingFantasyBooksFunc = fantasyBookFunc.withAuthor("Stephen King");
Book.AddTitle kingHorrorBooksFunc = horrorBookFunc.withAuthor("Stephen King");
Book.AddTitle rowlingFantasyBooksFunc = fantasyBookFunc.withAuthor("J.K. Rowling");

// Creates books by Stephen King (horror and fantasy genres)
Book shining = kingHorrorBooksFunc.withTitle("The Shining")
.withPublicationDate(LocalDate.of(1977, 1, 28));
Book darkTower = kingFantasyBooksFunc.withTitle("The Dark Tower: Gunslinger")
.withPublicationDate(LocalDate.of(1982, 6, 10));

// Creates fantasy books by J.K. Rowling
Book chamberOfSecrets = rowlingFantasyBooksFunc.withTitle("Harry Potter and the Chamber of Secrets")
.withPublicationDate(LocalDate.of(1998, 7, 2));

// Create sci-fi books
Book dune = scifiBookFunc.withAuthor("Frank Herbert")
.withTitle("Dune")
.withPublicationDate(LocalDate.of(1965, 8, 1));
Book foundation = scifiBookFunc.withAuthor("Isaac Asimov")
.withTitle("Foundation")
.withPublicationDate(LocalDate.of(1942, 5, 1));

// --snip--
}