0%

GoF设计模式(五):Builder Pattern 建造者模式

Builder Pattern 建造者模式,是创建型模式的一种。其核心内涵是将一个复杂对象的构建过程与表示分离,使得同样的构建过程可以创建出该对象的不同表示

abstract.jpeg

客户订车

在现实世界中,一个物品通常是由若干个子部分组成,例如电脑、汽车、手机等。对于一个物品的每个子部分来说,虽然有多种选择,但是各个子部分的组合通常又是固定的。以汽车为例,其包含轮胎、发动机、底盘等零部件。虽然每个零部件有很多型号可供选择。但现在有一个车厂,它只生产三款车型,每个车型零部件的组合如下所示。可以看到,对于任一一款车型而言,其各个零部件型号的组合是固定的。当客户向该车厂订购一辆车时,客户只需告知车厂订购的车型即可,而无需关心这辆车在制造时该用哪种型号的零部件

车型 轮胎 底盘 发动机
car1 tire1 chassis2 engine1
car2 tire2 chassis1 engine2
car3 tire1 chassis3 engine3

现在回到我们的更加熟悉的虚拟世界——软件开发,我们在构造一个对象时,经常显式地传入各种成员变量的参数来构造出一个我们所需的对象。但如果我们每次构建对象时,发现其成员变量参数的组合情况是一定的话,是不是就可以考虑借鉴一下我们上面所提到的客户订车的那个例子?经验告诉我们在软件开发中,大量重复的代码说明是有优化提升空间的

模式思想

现在我们好好想一想,我们能从客户订车这个小故事中能学到什么呢?首先对于客户来说,不论他订购哪种型号的车,他只需告诉车厂所订购的车型就好了;其次对于车厂而言,他不论制造什么型号的车,生产线的流程基本是固定的,比如这里可以是先装底盘、再装发动机、最后装轮胎。即构建过程是一致的,唯一区别的就是在制造不同车型的生产流水线上,所使用的零部件型号不同。这样就可以实现同样的生产线流程可以制造出不同型号的汽车

至此我们就从中提取到了一种思想,在软件开发领域我们称之为Builder Pattern建造者模式。在建造者模式中有4个角色:

  • Product : 产品,即最终被建造出的对象。就像上文所言的那样车厂最终是要生产出产品(即汽车)给客户的。同样地在软件开发中,作为一种创建型的设计模式,它的最终目的也是为了创建出一个对象
  • Director : 导演/指挥者。这个角色的目的是指挥建造者完成一个固定的建造流程。就像车厂里的生产流水线一样,无论哪种型号的车其所需经过工序都是一致的,流水线负责推动整个建造作业的流程顺序
  • Builder、ConcreteBuilder : 抽象建造者、具体建造者。车厂里三种车型都需要工人们来装底盘、装发动机、装轮胎,这些统一的行为就可以通过抽象建造者来约定,具体到软件开发中就是通过接口或抽象类来实现;而同一个行为在不同的生产线上会因为使用不同型号的零部件而产生差异,对应到软件开发中就是会有若干个具体建造者来实现成员变量参数的差异组合

小试牛刀

光说不练假把式,现在我们试着通过Builder Pattern建造者模式来实际演示下,以便让大家更好的理解这种设计模式,这里我们以手机为例进行说明,这里Phone类的角色就是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
/**
* 手机
*/
@Data // 可自动生成get、set、toString方法
public class Phone {

/**
* 品牌
*/
private String brand;

/**
* 操作系统
*/
private String os;

/**
* 内存大小, Unit: GB
*/
private Integer ramSize;

/**
* 售价
*/
private Double price;
}

我们可以看到Phone这个类包含了4个成员变量:品牌、操作系统、内存容量、售价,所以对于Builder抽象建造者来说,其目标就是定义设置各个成员变量的行为,以及建造完成后获取产品的行为 getPhone()。这里我们是通过接口来进行定义的

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
/**
* 手机抽象建造者的接口
*/
public interface PhoneBuilder {
/**
* 设置品牌
*/
void setBrand();

/**
* 设置操作系统
*/
void setOs();

/**
* 设置内存大小
*/
void setRamSize();

/**
* 设置售价
*/
void setPrice();

/**
* 获取 Phone 实例
* @return
*/
Phone getPhone();
}

现在就需要通过具体的建造者ConcreteBuilder来实现不同类型的手机建造,这里我们提供了3个具体建造者,用于建造3种不同类型的手机

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
/**
* 苹果手机建造者
*/
public class IPhoneBuilder implements PhoneBuilder{

private Phone phone;

public IPhoneBuilder() {
phone = new Phone();
}

@Override
public void setBrand() {
phone.setBrand("Apple");
}

@Override
public void setOs() {
phone.setOs("IOS");
}

@Override
public void setRamSize() {
phone.setRamSize(2);
}

@Override
public void setPrice() {
phone.setPrice(6666.66);
}

@Override
public Phone getPhone() {
return phone;
}
}
...
/**
* 小米手机建造者
*/
public class MiPhoneBuilder implements PhoneBuilder {

private Phone phone;

public MiPhoneBuilder() {
phone = new Phone();
}

@Override
public void setBrand() {
phone.setBrand("Xiao Mi");
}

@Override
public void setOs() {
phone.setOs("Android");
}

@Override
public void setRamSize() {
phone.setRamSize(8);
}

@Override
public void setPrice() {
phone.setPrice(1999.99);
}

@Override
public Phone getPhone() {
return phone;
}
}
...
/**
* 诺基亚手机建造者
*/
public class NokiaPhoneBuilder implements PhoneBuilder {

private Phone phone;

public NokiaPhoneBuilder() {
phone = new Phone();
}

@Override
public void setBrand() {
phone.setBrand("Nokia");
}

@Override
public void setOs() {
phone.setOs("Symbian");
}

@Override
public void setRamSize() {
phone.setRamSize(1);
}

@Override
public void setPrice() {
phone.setPrice(100.00);
}

@Override
public Phone getPhone() {
return phone;
}
}

至此,我们就差一个流水线——Director,来推动、指挥具体的建造者依次完成各个建造工序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 导演: 负责指定手机建造流程
*/
public class PhoneDirector {

/**
* 导演指挥建造者完成手机的建造工作
* @param phoneBuilder
*/
public void construct(PhoneBuilder phoneBuilder) {
phoneBuilder.setBrand();
phoneBuilder.setOs();
phoneBuilder.setRamSize();
phoneBuilder.setPrice();
}
}

现在,我们就完成了一个基于Builder Pattern的手机建造了。现在让我们一起来看看怎么订购手机吧,我们首先需要构造一个导演实例——phoneDirector,然后构造一个期望订购的手机的建造者。当我们把这个建造者交给导演时,就”自动”完成了手机的建造,我们这些客户再也不需要Care构造手机过程中成员变量的参数设置了。最后,当我们从建造者手中取出已经建造好的手机就可以了。至此,我们就完成了一个手机对象实例的构造过程

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
/**
* Builder Patter 建造者模式 Demo
*/
public class BuilderPatternDemo {

public static void demo1() {
// 1. 创建一个导演
PhoneDirector phoneDirector = new PhoneDirector();

/***** 2. 建造 iPhone 手机 *****/

// 2a. 创建一个 iPhone建造者
IPhoneBuilder iPhoneBuilder = new IPhoneBuilder();
// 2b. 导演指导 iPhone建造者 来建造一个iPhone的实例
phoneDirector.construct(iPhoneBuilder);
// 2c. 从 iPhone建造者 中获取实例
Phone iPhone = iPhoneBuilder.getPhone();
System.out.println(iPhone);

/***** 3. 建造 Xiao Mi Phone 手机 *****/

// 3a. 创建一个 MiPhone建造者
MiPhoneBuilder miPhoneBuilder = new MiPhoneBuilder();
// 3b. 导演指导 MiPhone建造者 来建造一个MiPhone的实例
phoneDirector.construct(miPhoneBuilder);
// 3c. 从 MiPhone建造者 中获取实例
Phone miPhone = miPhoneBuilder.getPhone();
System.out.println(miPhone);

/***** 4. 建造 Nokia Phone 手机 *****/

// 4a. 创建一个 Nokia Phone建造者
NokiaPhoneBuilder nokiaPhoneBuilder = new NokiaPhoneBuilder();
// 4b. 导演指导 NokiaPhone建造者 来建造一个NokiaPhone的实例
phoneDirector.construct(nokiaPhoneBuilder);
// 4c. 从 NokiaPhone建造者 中获取实例
Phone nokiaPhone = nokiaPhoneBuilder.getPhone();
System.out.println(nokiaPhone);
}
}

测试结果如下:

figure 1.jpeg

简化版 Builder Pattern

上文介绍的建造者模式,更多的是适用于子部分组合结果一定的场景。而对于子部分组合情况多样的场景就无法使用了。这样在构造对象实例时,要么通过构造器一次性进行初始化,要么不断地调用setXxxx方法进行初始化。当其成员变量较多时,就会显得代码十分繁琐冗长、不够优雅。为了解决这一问题,简化版 Builder Pattern 应运而生。这里我们同样以Phone为例通过使用简化版 Builder Pattern 重写该类

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
/**
* 手机
*/
@ToString // 可自动生成toString方法
public class Phone {

/**
* 品牌
*/
private String brand;

/**
* 操作系统
*/
private String os;

/**
* 内存大小, Unit: GB
*/
private Integer ramSize;

/**
* 售价
*/
private Double price;

/**
* 提供一个静态方法以方便创建一个Phone建造者实例
* @return
*/
public static Phone.PhoneBuilder builder() {
return new Phone.PhoneBuilder();
}

/**
* 提供一个Phone的全参构造器以供建造者Builder来建造Phone实例
* @param brand
* @param os
* @param ramSize
* @param price
*/
public Phone(String brand, String os, Integer ramSize, Double price) {
this.brand = brand;
this.os = os;
this.ramSize = ramSize;
this.price = price;
}


/**
* 静态内部类: Phone Builder 建造者
*/
public static class PhoneBuilder {

private String brand;

private String os;

private Integer ramSize;

private Double price;

/**
* Builder 建造者构造器
*/
public PhoneBuilder() {
}

public Phone.PhoneBuilder brand(String brand) {
this.brand = brand;
return this;
}

public Phone.PhoneBuilder os(String os) {
this.os = os;
return this;
}

public Phone.PhoneBuilder ramSize(Integer ramSize) {
this.ramSize = ramSize;
return this;
}

public Phone.PhoneBuilder price(Double price) {
this.price = price;
return this;
}

/**
* 建造者通过 Phone的全参构造器 来构造 Phone 实例
* @return
*/
public Phone build() {
return new Phone(brand, os, ramSize, price);
}
}
}

可以看到简化版 Builder Pattern 最明显的一个特点就是其不像传统 Builder Pattern 那样有多个类来扮演各个分工明显的角色。其只有一个Product类,然后通过内部类的方式实现了一个具体建造者。而Director导演这个角色也不需要了,其是通过”客户”按需的显式地指定建造流程。下面即是一个简化版的使用示例,相比较于构造器、setXxxx来进行初始化的姿势而言,链式调用的方式会显得更加简洁优雅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 简化版 Builder Patter 建造者模式 Demo
*/
public class BuilderPatterDemo {

public static void demo2() {

Phone P40Pro = Phone.builder() // 通过产品的静态方法获取建造者
.brand("华为") // "客户"(调用方)充当了Director这个角色
.os("鸿蒙")
.ramSize(12)
.price(9999.99)
.build(); // 获得建造完成的产品

System.out.println(P40Pro);
}
}

测试结果如下

figure 2.jpeg

Note

  1. Java开发者可以通过Lombok的@Builder注解自动生成该类简化版Builder Pattern的相关代码

参考文献

  1. Head First 设计模式 弗里曼著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝