建造者模式(Builder Pattern)
——.NET设计模式系列之四
Terrylee,2005年12月17日
概述
在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法确相对稳定。如何应对这种变化?如何提供一种“封装机制”来隔离出“复杂对象的各个部分”的变化,从而保持系统中的“稳定构建算法”不随着需求改变而改变?这就是要说的建造者模式。
本文通过现实生活中的买KFC的例子,用图解的方式来诠释建造者模式。
意图
将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
模型图
生活中的例子
生成器模式将复杂对象的构建与对象的表现分离开来,这样使得同样的构建过程可以创建出不同的表现。这种模式用于快餐店制作儿童餐。典型的儿童餐包括一个主食,一个辅食,一杯饮料和一个玩具(例如汉堡、炸鸡、可乐和玩具车)。这些在不同的儿童餐中可以是不同的,但是组合成儿童餐的过程是相同的。无论顾客点的是汉堡,三名治还是鸡肉,过程都是一样的。柜台的员工直接把主食,辅食和玩具放在一起。这些是放在一个袋子中的。饮料被倒入杯中,放在袋子外边。这些过程在相互竞争的餐馆中是同样的。
实现过程图解
在这里我们还是以去KFC店买套餐为例子,示意图如下:
客户端:顾客。想去买一套套餐(这里面包括汉堡,可乐,薯条),可以有1号和2号两种套餐供顾客选择。
指导者角色:收银员。知道顾客想要买什么样的套餐,并告诉餐馆员工去准备套餐。
建造者角色:餐馆员工。按照收银员的要求去准备具体的套餐,分别放入汉堡,可乐,薯条等。
产品角色:最后的套餐,所有的东西放在同一个盘子里面。
下面开始我们的买套餐过程。
1.客户创建Derector对象,并用它所想要的Builder对象进行配置。顾客进入KFC店要买套餐,先找到一个收银员,相当于创建了一个指导者对象。这位收银员给出两种套餐供顾客选择:1普通套餐,2黄金套餐。完成的工作如时序图中红色部分所示。
程序实现:
1using System; 2using System.Configuration; 3using System.Reflection; 4 5namespace KFC 6{ 7 /// <summary> 8 /// Client 类 9 /// </summary> 10 public class Client 11 { 12 public static void Main(string[] args) 13 { 14 FoodManager foodmanager = new FoodManager(); 15 16 Builder instance; 17 18 Console.WriteLine("Please Enter Food No:"); 19 20 string No = Console.ReadLine(); 21 22 string foodType = ConfigurationSettings.AppSettings["No"+No]; 23 24 instance = (Builder)Assembly.Load("KFC").CreateInstance("KFC." + foodType); 25 26 foodmanager.Construct(instance); 27 } 28 } 29} 30
产品(套餐)类:
1using System; 2using System.Collections; 3 4namespace KFC 5{ 6 /// <summary> 7 /// Food类,即产品类 8 /// </summary> 9 public class Food 10 { 11 Hashtable food = new Hashtable(); 12 13 /// <summary> 14 /// 添加食品 15 /// </summary> 16 /// <param name="strName">食品名称</param> 17 /// <param name="Price">价格</param> 18 public void Add(string strName,string Price) 19 { 20 food.Add(strName,Price); 21 } 22 23 /// <summary> 24 /// 显示食品清单 25 /// </summary> 26 public void Show() 27 { 28 IDictionaryEnumerator myEnumerator = food.GetEnumerator(); 29 Console.WriteLine("Food List:"); 30 Console.WriteLine("------------------------------"); 31 string strfoodlist = ""; 32 while(myEnumerator.MoveNext()) 33 { 34 strfoodlist = strfoodlist + " " + myEnumerator.Key.ToString(); 35 strfoodlist = strfoodlist + ": " +myEnumerator.Value.ToString(); 36 } 37 Console.WriteLine(strfoodlist); 38 Console.WriteLine(" ------------------------------"); 39 } 40 } 41} 42
2.指导者通知建造器。收银员(指导者)告知餐馆员工准备套餐。这里我们准备套餐的顺序是:放入汉堡,可乐倒入杯中,薯条放入盒中,并把这些东西都放在盘子上。这个过程对于普通套餐和黄金套餐来说都是一样的,不同的是它们的汉堡,可乐,薯条价格不同而已。如时序图红色部分所示:
程序实现:
1using System; 2 3namespace KFC 4{ 5 /// <summary> 6 /// FoodManager类,即指导者 7 /// </summary> 8 public class FoodManager 9 { 10 public void Construct(Builder builder) 11 { 12 builder.BuildHamb(); 13 14 builder.BuildCoke(); 15 16 builder.BuildChip(); 17 } 18 } 19} 20
3.建造者处理指导者的要求,并将部件添加到产品中。餐馆员工(建造者)按照收银员要求的把对应的汉堡,可乐,薯条放入盘子中。这部分是建造者模式里面富于变化的部分,因为顾客选择的套餐不同,套餐的组装过程也不同,这步完成产品对象的创建工作。
程序实现:
1using System; 2 3namespace KFC 4{ 5 /// <summary> 6 /// Builder类,即抽象建造者类,构造套餐 7 /// </summary> 8 public abstract class Builder 9 { 10 /// <summary> 11 /// 添加汉堡 12 /// </summary> 13 public abstract void BuildHamb(); 14 15 /// <summary> 16 /// 添加可乐 17 /// </summary> 18 public abstract void BuildCoke(); 19 20 /// <summary> 21 /// 添加薯条 22 /// </summary> 23 public abstract void BuildChip(); 24 25 /// <summary> 26 /// 返回结果 27 /// </summary> 28 /// <returns>食品对象</returns> 29 public abstract Food GetFood(); 30 } 31} 32
1using System; 2 3namespace KFC 4{ 5 /// <summary> 6 /// NormalBuilder类,具体构造者,普通套餐 7 /// </summary> 8 public class NormalBuilder:Builder 9 { 10 private Food NormalFood = new Food(); 11 12 public override void BuildHamb() 13 { 14 NormalFood.Add("NormalHamb","¥10.50"); 15 } 16 17 public override void BuildCoke() 18 { 19 NormalFood.Add("CokeCole","¥4.50"); 20 } 21 22 public override void BuildChip() 23 { 24 NormalFood.Add("FireChips","¥2.00"); 25 } 26 27 public override Food GetFood() 28 { 29 return NormalFood; 30 } 31 32 } 33} 34
1using System; 2 3namespace KFC 4{ 5 /// <summary> 6 /// GoldBuilder类,具体构造者,黄金套餐 7 /// </summary> 8 public class GoldBuilder:Builder 9 { 10 private Food GoldFood = new Food(); 11 12 public override void BuildHamb() 13 { 14 GoldFood.Add("GoldHamb","¥13.50"); 15 } 16 17 public override void BuildCoke() 18 { 19 GoldFood.Add("CokeCole","¥4.50"); 20 } 21 22 public override void BuildChip() 23 { 24 GoldFood.Add("FireChips","¥3.50"); 25 } 26 27 public override Food GetFood() 28 { 29 return GoldFood; 30 } 31 32 } 33} 34
4.客户从建造者检索产品。从餐馆员工准备好套餐后,顾客再从餐馆员工那儿拿回套餐。这步客户程序要做的仅仅是取回已经生成的产品对象,如时序图中红色部分所示。
完整的客户程序:
1using System; 2using System.Configuration; 3using System.Reflection; 4 5namespace KFC 6{ 7 /// <summary> 8 /// Client 类 9 /// </summary> 10 public class Client 11 { 12 public static void Main(string[] args) 13 { 14 FoodManager foodmanager = new FoodManager(); 15 16 Builder instance; 17 18 Console.WriteLine("Please Enter Food No:"); 19 20 string No = Console.ReadLine(); 21 22 string foodType = ConfigurationSettings.AppSettings["No"+No]; 23 24 instance = (Builder)Assembly.Load("KFC").CreateInstance("KFC." + foodType); 25 26 foodmanager.Construct(instance); 27 28 Food food = instance.GetFood(); 29 food.Show(); 30 31 Console.ReadLine(); 32 } 33 } 34} 35
通过分析不难看出,在这个例子中,在准备套餐的过程是稳定的,即按照一定的步骤去做,而套餐的组成部分则是变化的,有可能是普通套餐或黄金套餐等。这个变化就是建造者模式中的“变化点“,就是我们要封装的部分。
另外一个例子
在这里我们再给出另外一个关于建造房子的例子。客户程序通过调用指导者 (CDirector class)的BuildHouse()方法来创建一个房子。该方法有一个布尔型的参数blnBackyard,当blnBackyard为假时指导者将创建一个Apartment(Concrete Builder),当它为真时将创建一个Single Family Home(Concrete Builder)。这两种房子都实现了接口Ihouse。
程序实现:
1//关于建造房屋的例子 2using System; 3using System.Collections; 4 5/// <summary> 6/// 抽象建造者 7/// </summary> 8public interface IHouse 9{ 10 bool GetBackyard(); 11 long NoOfRooms(); 12 string Description(); 13} 14 15/// <summary> 16/// 具体建造者 17/// </summary> 18public class CApt:IHouse 19{ 20 private bool mblnBackyard; 21 private Hashtable Rooms; 22 public CApt() 23 { 24 CRoom room; 25 Rooms = new Hashtable(); 26 room = new CRoom(); 27 room.RoomName = "Master Bedroom"; 28 Rooms.Add ("room1",room); 29 30 room = new CRoom(); 31 room.RoomName = "Second Bedroom"; 32 Rooms.Add ("room2",room); 33 34 room = new CRoom(); 35 room.RoomName = "Living Room"; 36 Rooms.Add ("room3",room); 37 38 mblnBackyard = false; 39 } 40 41 public bool GetBackyard() 42 { 43 return mblnBackyard; 44 } 45 public long NoOfRooms() 46 { 47 return Rooms.Count; 48 } 49 public string Description() 50 { 51 IDictionaryEnumerator myEnumerator = Rooms.GetEnumerator(); 52 string strDescription; 53 strDescription = "This is an Apartment with " + Rooms.Count + " Rooms "; 54 strDescription = strDescription + "This Apartment doesn't have a backyard "; 55 while (myEnumerator.MoveNext()) 56 { 57 strDescription = strDescription + " " + myEnumerator.Key + " " + ((CRoom)myEnumerator.Value).RoomName; 58 } 59 return strDescription; 60 } 61} 62 63/// <summary> 64/// 具体建造者 65/// </summary> 66public class CSFH:IHouse 67{ 68 private bool mblnBackyard; 69 private Hashtable Rooms; 70 public CSFH() 71 { 72 CRoom room; 73 Rooms = new Hashtable(); 74 75 room = new CRoom(); 76 room.RoomName = "Master Bedroom"; 77 Rooms.Add ("room1",room); 78 79 room = new CRoom(); 80 room.RoomName = "Second Bedroom"; 81 Rooms.Add ("room2",room); 82 83 room = new CRoom(); 84 room.RoomName = "Third Room"; 85 Rooms.Add ("room3",room); 86 87 room = new CRoom(); 88 room.RoomName = "Living Room"; 89 Rooms.Add ("room4",room); 90 91 room = new CRoom(); 92 room.RoomName = "Guest Room"; 93 Rooms.Add ("room5",room); 94 95 mblnBackyard = true; 96 97 } 98 99 public bool GetBackyard() 100 { 101 return mblnBackyard; 102 } 103 public long NoOfRooms() 104 { 105 return Rooms.Count; 106 } 107 public string Description() 108 { 109 IDictionaryEnumerator myEnumerator = Rooms.GetEnumerator(); 110 string strDescription; 111 strDescription = "This is an Single Family Home with " + Rooms.Count + " Rooms "; 112 strDescription = strDescription + "This house has a backyard "; 113 while (myEnumerator.MoveNext()) 114 { 115 strDescription = strDescription + " " + myEnumerator.Key + " " + ((CRoom)myEnumerator.Value).RoomName; 116 } 117 return strDescription; 118 } 119} 120 121public interface IRoom 122{ 123 string RoomName{get;set;} 124} 125 126public class CRoom:IRoom 127{ 128 private string mstrRoomName; 129 public string RoomName 130 { 131 get 132 { 133 return mstrRoomName; 134 } 135 set 136 { 137 mstrRoomName = value; 138 } 139 } 140} 141 142/// <summary> 143/// 指导者 144/// </summary> 145public class CDirector 146{ 147 public IHouse BuildHouse(bool blnBackyard) 148 { 149 if (blnBackyard) 150 { 151 return new CSFH(); 152 } 153 else 154 { 155 return new CApt(); 156 } 157 } 158} 159 160/// <summary> 161/// 客户程序 162/// </summary> 163public class Client 164{ 165 static void Main(string[] args) 166 { 167 CDirector objDirector = new CDirector(); 168 IHouse objHouse; 169 170 string Input = Console.ReadLine(); 171 objHouse = objDirector.BuildHouse(bool.Parse(Input)); 172 173 Console.WriteLine(objHouse.Description()); 174 Console.ReadLine(); 175 } 176} 177 178
建造者模式的几种演化
省略抽象建造者角色
系统中只需要一个具体建造者,省略掉抽象建造者,结构图如下:
指导者代码如下:
1 class Director 2 { 3 private ConcreteBuilder builder; 4 5 public void Construct() 6 { 7 builder.BuildPartA(); 8 builder.BuildPartB(); 9 } 10 }
省略指导者角色
抽象建造者角色已经被省略掉,还可以省略掉指导者角色。让Builder角色自己扮演指导者与建造者双重角色。结构图如下:
建造者角色代码如下:
1 public class Builder 2 { 3 private Product product = new Product(); 4 5 public void BuildPartA() 6 { 7 // 8 } 9 10 public void BuildPartB() 11 { 12 // 13 } 14 15 public Product GetResult() 16 { 17 return product; 18 } 19 20 public void Construct() 21 { 22 BuildPartA(); 23 BuildPartB(); 24 } 25 }
客户程序:
1 public class Client 2 { 3 private static Builder builder; 4 5 public static void Main() 6 { 7 builder = new Builder(); 8 builder.Construct(); 9 Product product = builder.GetResult(); 10 } 11 }
合并建造者角色和产品角色
建造模式失去抽象建造者角色和指导者角色后,可以进一步退化,从而失去具体建造者角色,此时具体建造者角色和产品角色合并,从而使得产品自己就是自己的建造者。这样做混淆了对象的建造者和对象本身,但是有时候一个产品对象有着固定的几个零件,而且永远只有这几个零件,此时将产品类和建造类合并,可以使系统简单易读。结构图如下:
实现要点
1、建造者模式主要用于“分步骤构建一个复杂的对象”,在这其中“分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化。
2、产品不需要抽象类,特别是由于创建对象的算法复杂而导致使用此模式的情况下或者此模式应用于产品的生成过程,其最终结果可能差异很大,不大可能提炼出一个抽象产品类。
3、创建者中的创建子部件的接口方法不是抽象方法而是空方法,不进行任何操作,具体的创建者只需要覆盖需要的方法就可以,但是这也不是绝对的,特别是类似文本转换这种情况下,缺省的方法将输入原封不动的输出是合理的缺省操作。
4、前面我们说过的抽象工厂模式(Abtract Factory)解决“系列对象”的需求变化,Builder模式解决“对象部分”的需求变化,建造者模式常和组合模式(Composite Pattern)结合使用。
效果
1、建造者模式的使用使得产品的内部表象可以独立的变化。使用建造者模式可以使客户端不必知道产品内部组成的细节。
2、每一个Builder都相对独立,而与其它的Builder无关。
3、可使对构造过程更加精细控制。
4、将构建代码和表示代码分开。
5、建造者模式的缺点在于难于应付“分步骤构建算法”的需求变动。
适用性
以下情况应当使用建造者模式:
1、需要生成的产品对象有复杂的内部结构。 2、需要生成的产品对象的属性相互依赖,建造者模式可以强迫生成顺序。 3、 在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到。
应用场景
1、 RTF文档交换格式阅读器。
2、 .NET环境下的字符串处理StringBuilder,这是一种简化了的建造者模式。
3、 ……
总结
建造者模式的实质是解耦组装过程和创建具体部件,使得我们不用去关心每个部件是如何组装的。
______________________________________________________________________________________
源程序下载:/Files/Terrylee/BuilderPattern.rar
参考资料:
《Java与设计模式》阎宏 著
《设计模式(中文版)》
《DesignPatternsExplained》