——.NET设计模式系列之四
Terrylee
,
2005
年
12
月
17
日
概述
在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法确相对稳定。如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?这就是要说的建造者模式。
本文通过现实生活中的买
KFC
的例子,用图解的方式来诠释建造者模式。
意图
将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
模型图

生活中的例子
生成器模式将复杂对象的构建与对象的表现分离开来,这样使得同样的构建过程可以创建出不同的表现。这种模式用于快餐店制作儿童餐。典型的儿童餐包括一个主食,一个辅食,一杯饮料和一个玩具(例如汉堡、炸鸡、可乐和玩具车)。这些在不同的儿童餐中可以是不同的,但是组合成儿童餐的过程是相同的。无论顾客点的是汉堡,三名治还是鸡肉,过程都是一样的。柜台的员工直接把主食,辅食和玩具放在一起。这些是放在一个袋子中的。饮料被倒入杯中,放在袋子外边。这些过程在相互竞争的餐馆中是同样的。

实现过程图解
在这里我们还是以去
KFC
店买套餐为例子,示意图如下:

客户端:顾客。想去买一套套餐(这里面包括汉堡,可乐,薯条),可以有
1
号和
2
号两种套餐供顾客选择。
指导者角色:收银员。知道顾客想要买什么样的套餐,并告诉餐馆员工去准备套餐。
建造者角色:餐馆员工。按照收银员的要求去准备具体的套餐,分别放入汉堡,可乐,薯条等。
产品角色:最后的套餐,所有的东西放在同一个盘子里面。
下面开始我们的买套餐过程。
1
.客户创建
Derector
对象,并用它所想要的
Builder
对象进行配置。顾客进入
KFC
店要买套餐,先找到一个收银员,相当于创建了一个指导者对象。这位收银员给出两种套餐供顾客选择:
1
普通套餐,
2
黄金套餐。完成的工作如时序图中红色部分所示。

程序实现:
1
using
System;
2
using
System.Configuration;
3
using
System.Reflection;
4
5
namespace
KFC
6
{
7
/**////<summary>
8
///Client类
9
///</summary>
10
publicclassClient
11

{
12
publicstaticvoidMain(string[]args)
13

{
14
FoodManagerfoodmanager=newFoodManager();
15
16
Builderinstance;
17
18
Console.WriteLine("PleaseEnterFoodNo:");
19
20
stringNo=Console.ReadLine();
21
22
stringfoodType=ConfigurationSettings.AppSettings["No"+No];
23
24
instance=(Builder)Assembly.Load("KFC").CreateInstance("KFC."+foodType);
25
26
foodmanager.Construct(instance);
27
}
28
}
29
}
30

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

产品(套餐)类:
.指导者通知建造器。收银员(指导者)告知餐馆员工准备套餐。这里我们准备套餐的顺序是:放入汉堡,可乐倒入杯中,薯条放入盒中,并把这些东西都放在盘子上。这个过程对于普通套餐和黄金套餐来说都是一样的,不同的是它们的汉堡,可乐,薯条价格不同而已。如时序图红色部分所示:
.指导者通知建造器。收银员(指导者)告知餐馆员工准备套餐。这里我们准备套餐的顺序是:放入汉堡,可乐倒入杯中,薯条放入盒中,并把这些东西都放在盘子上。这个过程对于普通套餐和黄金套餐来说都是一样的,不同的是它们的汉堡,可乐,薯条价格不同而已。如时序图红色部分所示:
1
using
System;
2
using
System.Collections;
3
4
namespace
KFC
5
{
6
/**////<summary>
7
///Food类,即产品类
8
///</summary>
9
publicclassFood
10

{
11
Hashtablefood=newHashtable();
12
13
/**////<summary>
14
///添加食品
15
///</summary>
16
///<paramname="strName">食品名称</param>
17
///<paramname="Price">价格</param>
18
publicvoidAdd(stringstrName,stringPrice)
19

{
20
food.Add(strName,Price);
21
}
22
23
/**////<summary>
24
///显示食品清单
25
///</summary>
26
publicvoidShow()
27

{
28
IDictionaryEnumeratormyEnumerator=food.GetEnumerator();
29
Console.WriteLine("FoodList:");
30
Console.WriteLine("------------------------------");
31
stringstrfoodlist="";
32
while(myEnumerator.MoveNext())
33

{
34
strfoodlist=strfoodlist+"/n/n"+myEnumerator.Key.ToString();
35
strfoodlist=strfoodlist+":/t"+myEnumerator.Value.ToString();
36
}
37
Console.WriteLine(strfoodlist);
38
Console.WriteLine("/n------------------------------");
39
}
40
}
41
}
42

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

2

程序实现:
1
using
System;
2
3
namespace
KFC
4
{
5
/**////<summary>
6
///FoodManager类,即指导者
7
///</summary>
8
publicclassFoodManager
9

{
10
publicvoidConstruct(Builderbuilder)
11

{
12
builder.BuildHamb();
13
14
builder.BuildCoke();
15
16
builder.BuildChip();
17
}
18
}
19
}
20

2

3

4



5


6

7

8

9



10

11



12

13

14

15

16

17

18

19

20

3
.建造者处理指导者的要求,并将部件添加到产品中。餐馆员工(建造者)按照收银员要求的把对应的汉堡,可乐,薯条放入盘子中。这部分是建造者模式里面富于变化的部分,因为顾客选择的套餐不同,套餐的组装过程也不同,这步完成产品对象的创建工作。
程序实现:
1
using
System;
2
3
namespace
KFC
4
{
5
/**////<summary>
6
///Builder类,即抽象建造者类,构造套餐
7
///</summary>
8
publicabstractclassBuilder
9

{
10
/**////<summary>
11
///添加汉堡
12
///</summary>
13
publicabstractvoidBuildHamb();
14
15
/**////<summary>
16
///添加可乐
17
///</summary>
18
publicabstractvoidBuildCoke();
19
20
/**////<summary>
21
///添加薯条
22
///</summary>
23
publicabstractvoidBuildChip();
24
25
/**////<summary>
26
///返回结果
27
///</summary>
28
///<returns>食品对象</returns>
29
publicabstractFoodGetFood();
30
}
31
}
32

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

1
using
System;
2
3
namespace
KFC
4
{
5
/**////<summary>
6
///NormalBuilder类,具体构造者,普通套餐
7
///</summary>
8
publicclassNormalBuilder:Builder
9

{
10
privateFoodNormalFood=newFood();
11
12
publicoverridevoidBuildHamb()
13

{
14
NormalFood.Add("NormalHamb","¥10.50");
15
}
16
17
publicoverridevoidBuildCoke()
18

{
19
NormalFood.Add("CokeCole","¥4.50");
20
}
21
22
publicoverridevoidBuildChip()
23

{
24
NormalFood.Add("FireChips","¥2.00");
25
}
26
27
publicoverrideFoodGetFood()
28

{
29
returnNormalFood;
30
}
31
32
}
33
}
34

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

1
using
System;
2
3
namespace
KFC
4
{
5
/**////<summary>
6
///GoldBuilder类,具体构造者,黄金套餐
7
///</summary>
8
publicclassGoldBuilder:Builder
9

{
10
privateFoodGoldFood=newFood();
11
12
publicoverridevoidBuildHamb()
13

{
14
GoldFood.Add("GoldHamb","¥13.50");
15
}
16
17
publicoverridevoidBuildCoke()
18

{
19
GoldFood.Add("CokeCole","¥4.50");
20
}
21
22
publicoverridevoidBuildChip()
23

{
24
GoldFood.Add("FireChips","¥3.50");
25
}
26
27
publicoverrideFoodGetFood()
28

{
29
returnGoldFood;
30
}
31
32
}
33
}
34

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

4
.客户从建造者检索产品。从餐馆员工准备好套餐后,顾客再从餐馆员工那儿拿回套餐。这步客户程序要做的仅仅是取回已经生成的产品对象,如时序图中红色部分所示。

完整的客户程序:
1
using
System;
2
using
System.Configuration;
3
using
System.Reflection;
4
5
namespace
KFC
6
{
7
/**////<summary>
8
///Client类
9
///</summary>
10
publicclassClient
11

{
12
publicstaticvoidMain(string[]args)
13

{
14
FoodManagerfoodmanager=newFoodManager();
15
16
Builderinstance;
17
18
Console.WriteLine("PleaseEnterFoodNo:");
19
20
stringNo=Console.ReadLine();
21
22
stringfoodType=ConfigurationSettings.AppSettings["No"+No];
23
24
instance=(Builder)Assembly.Load("KFC").CreateInstance("KFC."+foodType);
25
26
foodmanager.Construct(instance);
27
28
Foodfood=instance.GetFood();
29
food.Show();
30
31
Console.ReadLine();
32
}
33
}
34
}
35

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

通过分析不难看出,在这个例子中,在准备套餐的过程是稳定的,即按照一定的步骤去做,而套餐的组成部分则是变化的,有可能是普通套餐或黄金套餐等。这个变化就是建造者模式中的“变化点“,就是我们要封装的部分。
另外一个例子
在这里我们再给出另外一个关于建造房子的例子。客户程序通过调用
指导者
(CDirector class)
的
BuildHouse()
方法来创建一个房子。该方法有一个布尔型的参数
blnBackyard
,当
blnBackyard
为假时指导者将创建一个
Apartment
(
Concrete Builder
),当它为真时将创建一个
Single Family Home
(
Concrete Builder
)。这两种房子都实现了接口
Ihouse
。
程序实现:
1
//
关于建造房屋的例子
2
using
System;
3
using
System.Collections;
4
5
/**/
///<summary>
6
///抽象建造者
7
///</summary>
8
public
interface
IHouse
9
{
10
boolGetBackyard();
11
longNoOfRooms();
12
stringDescription();
13
}
14
15
/**/
///<summary>
16
///具体建造者
17
///</summary>
18
public
class
CApt:IHouse
19
{
20
privateboolmblnBackyard;
21
privateHashtableRooms;
22
publicCApt()
23

{
24
CRoomroom;
25
Rooms=newHashtable();
26
room=newCRoom();
27
room.RoomName="MasterBedroom";
28
Rooms.Add("room1",room);
29
30
room=newCRoom();
31
room.RoomName="SecondBedroom";
32
Rooms.Add("room2",room);
33
34
room=newCRoom();
35
room.RoomName="LivingRoom";
36
Rooms.Add("room3",room);
37
38
mblnBackyard=false;
39
}
40
41
publicboolGetBackyard()
42

{
43
returnmblnBackyard;
44
}
45
publiclongNoOfRooms()
46

{
47
returnRooms.Count;
48
}
49
publicstringDescription()
50

{
51
IDictionaryEnumeratormyEnumerator=Rooms.GetEnumerator();
52
stringstrDescription;
53
strDescription="ThisisanApartmentwith"+Rooms.Count+"Rooms/n";
54
strDescription=strDescription+"ThisApartmentdoesn'thaveabackyard/n";
55
while(myEnumerator.MoveNext())
56

{
57
strDescription=strDescription+"/n"+myEnumerator.Key+"/t"+((CRoom)myEnumerator.Value).RoomName;
58
}
59
returnstrDescription;
60
}
61
}
62
63
/**/
///<summary>
64
///具体建造者
65
///</summary>
66
public
class
CSFH:IHouse
67
{
68
privateboolmblnBackyard;
69
privateHashtableRooms;
70
publicCSFH()
71

{
72
CRoomroom;
73
Rooms=newHashtable();
74
75
room=newCRoom();
76
room.RoomName="MasterBedroom";
77
Rooms.Add("room1",room);
78
79
room=newCRoom();
80
room.RoomName="SecondBedroom";
81
Rooms.Add("room2",room);
82
83
room=newCRoom();
84
room.RoomName="ThirdRoom";
85
Rooms.Add("room3",room);
86
87
room=newCRoom();
88
room.RoomName="LivingRoom";
89
Rooms.Add("room4",room);
90
91
room=newCRoom();
92
room.RoomName="GuestRoom";
93
Rooms.Add("room5",room);
94
95
mblnBackyard=true;
96
97
}
98
99
publicboolGetBackyard()
100

{
101
returnmblnBackyard;
102
}
103
publiclongNoOfRooms()
104

{
105
returnRooms.Count;
106
}
107
publicstringDescription()
108

{
109
IDictionaryEnumeratormyEnumerator=Rooms.GetEnumerator();
110
stringstrDescription;
111
strDescription="ThisisanSingleFamilyHomewith"+Rooms.Count+"Rooms/n";
112
strDescription=strDescription+"Thishousehasabackyard/n";
113
while(myEnumerator.MoveNext())
114

{
115
strDescription=strDescription+"/n"+myEnumerator.Key+"/t"+((CRoom)myEnumerator.Value).RoomName;
116
}
117
returnstrDescription;
118
}
119
}
120
121
public
interface
IRoom
122
{
123
stringRoomName
{get;set;}
124
}
125
126
public
class
CRoom:IRoom
127
{
128
privatestringmstrRoomName;
129
publicstringRoomName
130

{
131
get
132

{
133
returnmstrRoomName;
134
}
135
set
136

{
137
mstrRoomName=value;
138
}
139
}
140
}
141
142
/**/
///<summary>
143
///指导者
144
///</summary>
145
public
class
CDirector
146
{
147
publicIHouseBuildHouse(boolblnBackyard)
148

{
149
if(blnBackyard)
150

{
151
returnnewCSFH();
152
}
153
else
154

{
155
returnnewCApt();
156
}
157
}
158
}
159
160
/**/
///<summary>
161
///客户程序
162
///</summary>
163
public
class
Client
164
{
165
staticvoidMain(string[]args)
166

{
167
CDirectorobjDirector=newCDirector();
168
IHouseobjHouse;
169
170
stringInput=Console.ReadLine();
171
objHouse=objDirector.BuildHouse(bool.Parse(Input));
172
173
Console.WriteLine(objHouse.Description());
174
Console.ReadLine();
175
}
176
}
177
178

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

117

118

119

120

121

122



123



124

125

126

127



128

129

130



131

132



133

134

135

136



137

138

139

140

141

142


143

144

145

146



147

148



149

150



151

152

153

154



155

156

157

158

159

160


161

162

163

164



165

166



167

168

169

170

171

172

173

174

175

176

177

178

建造者模式的几种演化
省略抽象建造者角色
系统中只需要一个具体建造者,省略掉抽象建造者,结构图如下:

指导者代码如下:
1
class
Director
2
{
3
privateConcreteBuilderbuilder;
4
5
publicvoidConstruct()
6

{
7
builder.BuildPartA();
8
builder.BuildPartB();
9
}
10
}

2



3

4

5

6



7

8

9

10

省略指导者角色
抽象建造者角色已经被省略掉,还可以省略掉指导者角色。让
Builder
角色自己扮演指导者与建造者双重角色。结构图如下:

建造者角色代码如下:
1
public
class
Builder
2
{
3
privateProductproduct=newProduct();
4
5
publicvoidBuildPartA()
6

{
7
//

8
}
9
10
publicvoidBuildPartB()
11

{
12
//

13
}
14
15
publicProductGetResult()
16

{
17
returnproduct;
18
}
19
20
publicvoidConstruct()
21

{
22
BuildPartA();
23
BuildPartB();
24
}
25
}

2



3

4

5

6



7



8

9

10

11



12



13

14

15

16



17

18

19

20

21



22

23

24

25

客户程序:
1
public
class
Client
2
{
3
privatestaticBuilderbuilder;
4
5
publicstaticvoidMain()
6

{
7
builder=newBuilder();
8
builder.Construct();
9
Productproduct=builder.GetResult();
10
}
11
}

2



3

4

5

6



7

8

9

10

11

合并建造者角色和产品角色
建造模式失去抽象建造者角色和指导者角色后,可以进一步退化,从而失去具体建造者角色,此时具体建造者角色和产品角色合并,从而使得产品自己就是自己的建造者。这样做混淆了对象的建造者和对象本身,但是有时候一个产品对象有着固定的几个零件,而且永远只有这几个零件,此时将产品类和建造类合并,可以使系统简单易读。结构图如下:

实现要点
1
、建造者模式主要用于“分步骤构建一个复杂的对象”,在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化。
2
、
产品不需要抽象类,特别是由于创建对象的算法复杂而导致使用此模式的情况下或者此模式应用于产品的生成过程,其最终结果可能差异很大,不大可能提炼出一个抽象产品类。
3、创建者中的创建子部件的接口方法不是抽象方法而是空方法,不进行任何操作,具体的创建者只需要覆盖需要的方法就可以,但是这也不是绝对的,特别是类似文本转换这种情况下,缺省的方法将输入原封不动的输出是合理的缺省操作。
3、创建者中的创建子部件的接口方法不是抽象方法而是空方法,不进行任何操作,具体的创建者只需要覆盖需要的方法就可以,但是这也不是绝对的,特别是类似文本转换这种情况下,缺省的方法将输入原封不动的输出是合理的缺省操作。
4
、前面我们说过的抽象工厂模式(Abtract Factory)解决“系列对象”的需求变化,Builder模式解决“对象部分”的需求变化,建造者模式常和组合模式(Composite Pattern)结合使用。
效果
1
、建造者模式的使用使得产品的内部表象可以独立的变化。使用建造者模式可以使客户端不必知道产品内部组成的细节。
2 、每一个 Builder 都相对独立,而与其它的 Builder 无关。
3 、可使对构造过程更加精细控制。
2 、每一个 Builder 都相对独立,而与其它的 Builder 无关。
3 、可使对构造过程更加精细控制。
4
、将构建代码和表示代码分开。
5
、建造者模式的缺点在于难于应付“分步骤构建算法”的需求变动。
适用性
以下情况应当使用建造者模式:
1
、需要生成的产品对象有复杂的内部结构。
2 、需要生成的产品对象的属性相互依赖,建造者模式可以强迫生成顺序。
3 、 在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到。
2 、需要生成的产品对象的属性相互依赖,建造者模式可以强迫生成顺序。
3 、 在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到。
应用场景
1、
RTF
文档交换格式阅读器。
2、
.NET
环境下的字符串处理
StringBuilder
,这是一种简化了的建造者模式。
3、
……
总结
建造者模式的实质是解耦组装过程和创建具体部件,使得我们不用去关心每个部件是如何组装的。
______________________________________________________________________________________
参考资料:
《
Java
与设计模式》阎宏
著
《设计模式(中文版)》
《
DesignPatternsExplained
》