学习设计模式第六 - 建造者模式
本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法确相对稳定。如何应对这种变化?如何提供一种"封装机制"来隔离出"复杂对象的各个部分"的变化,从而保持系统中的"稳定构建算法"不随着需求改变而改变?这就是要说的建造者模式。
后文将通过现实生活中的买KFC的例子,用图解的方式来诠释建造者模式。
意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
UML
图1.建造者模式UML图
参与者
这个模式涉及的类或对象:
-
Builder
-
定义创建产品各部分需要操作的抽象接口
-
ConcreteBuilder
-
实现Builder接口,以实际构建并组装产品的每一部分
-
定义并跟踪其创建的对象的表示
-
提供一个接口供检索产品
-
Director
-
使用Builder接口构造一个对象
-
Product
-
表示被构建的复杂对象。ConcreteBuilder生成这个产品的内部表示并按定义的过程进行组装。
-
包含定义了组成部分的类型,包括用于将每一部分最终组装到一起的接口。
适用性
使用建造者模式,客户端只需传入类型或内容就可以构造一个复杂的对象。构建细节对于客户端完全透明。使用建造者模式最主要的原因就是使用尽可能简单的代码创建复杂的对象。客户端仍然可以指导Builder的建造步骤,而不需要指导实际工作如何被完成。Builder模式常用于封装组合对象(另一种设计模式)的构造,因为后者中的生产者常常是重复且复杂的。简言之,建造者模式主要用于创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。
一个建造者模式发挥作用的场景是用于代码生成器时。例如,你正编写一个为不同目标数据库(SQL Server, Oracle, Db2)编写存储过程的程序。最终输出完全不同,但创建存储过程的每个步骤(增,删,改,查)是相似的。使用建造者模式可以对构建过程每一步有更大的控制,而工厂模式中之能给出一个创建步骤。
以下情况应当使用建造者模式:
-
需要生成的产品对象有复杂的内部结构。
-
需要生成的产品对象的属性相互依赖,建造者模式可以强迫生成顺序。
-
在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到。
-
当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
-
当构造过程必须允许被构造的对象有不同的表示时。
建造者模式是在当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装备方式时适用的模式。
DoFactory GoF代码
这个例子中产品需要经过多步构建,构建过程可以产生不同表示的对象。而这个建立不同表示的任务正是由具体建造者来完成。
// Builder pattern // Structural example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Builder.Structural { public class MainApp { public static void Main() { // Create director and builders Director director = new Director(); Builder b1 = new ConcreteBuilder1(); Builder b2 = new ConcreteBuilder2(); // Construct two products director.Construct(b1); Product p1 = b1.GetResult(); p1.Show(); director.Construct(b2); Product p2 = b2.GetResult(); p2.Show(); // Wait for user Console.ReadKey(); } } // "Director" class Director { // Builder uses a complex series of steps public void Construct(Builder builder) { builder.BuildPartA(); builder.BuildPartB(); } } // "Builder" abstract class Builder { public abstract void BuildPartA(); public abstract void BuildPartB(); public abstract Product GetResult(); } // "ConcreteBuilder1" class ConcreteBuilder1 : Builder { private Product _product = new Product(); public override void BuildPartA() { _product.Add("PartA"); } public override void BuildPartB() { _product.Add("PartB"); } public override Product GetResult() { return _product; } } // "ConcreteBuilder2" class ConcreteBuilder2 : Builder { private Product _product = new Product(); public override void BuildPartA() { _product.Add("PartX"); } public override void BuildPartB() { _product.Add("PartY"); } public override Product GetResult() { return _product; } } // "Product" class Product { private List<string> _parts = new List<string>(); public void Add(string part) { _parts.Add(part); } public void Show() { Console.WriteLine("\nProduct Parts -------"); foreach (string part in _parts) Console.WriteLine(part); } } }
这个例子中演示了使用一系列相同的步骤但每一步有不同的具体操作来生成不同种类的车辆的过程。例子中涉及到的类与建造者模式中标准的类对应关系如下:
-
Builder – VehicleBuilder
-
ConcreteBuilder – MotorCycleBuilder,CarBuilder,ScooterBuilder
-
Director - Shop
-
Product - Vehicle
// Builder pattern // Real World example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Builder.RealWorld { public class MainApp { public static void Main() { VehicleBuilder builder; // Create shop with vehicle builders Shop shop = new Shop(); // Construct and display vehicles builder = new ScooterBuilder(); shop.Construct(builder); builder.Vehicle.Show(); builder = new CarBuilder(); shop.Construct(builder); builder.Vehicle.Show(); builder = new MotorCycleBuilder(); shop.Construct(builder); builder.Vehicle.Show(); // Wait for user Console.ReadKey(); } } // "Director" class Shop { // Builder uses a complex series of steps public void Construct(VehicleBuilder vehicleBuilder) { vehicleBuilder.BuildFrame(); vehicleBuilder.BuildEngine(); vehicleBuilder.BuildWheels(); vehicleBuilder.BuildDoors(); } } // "Builder" abstract class abstract class VehicleBuilder { protected Vehicle vehicle; // Gets vehicle instance public Vehicle Vehicle { get { return vehicle; } } // Abstract build methods public abstract void BuildFrame(); public abstract void BuildEngine(); public abstract void BuildWheels(); public abstract void BuildDoors(); } // "ConcreteBuilder1" class MotorCycleBuilder : VehicleBuilder { public MotorCycleBuilder() { vehicle = new Vehicle("MotorCycle"); } public override void BuildFrame() { vehicle["frame"] = "MotorCycle Frame"; } public override void BuildEngine() { vehicle["engine"] = "500 cc"; } public override void BuildWheels() { vehicle["wheels"] = "2"; } public override void BuildDoors() { vehicle["doors"] = "0"; } } // "ConcreteBuilder2" class CarBuilder : VehicleBuilder { public CarBuilder() { vehicle = new Vehicle("Car"); } public override void BuildFrame() { vehicle["frame"] = "Car Frame"; } public override void BuildEngine() { vehicle["engine"] = "2500 cc"; } public override void BuildWheels() { vehicle["wheels"] = "4"; } public override void BuildDoors() { vehicle["doors"] = "4"; } } // "ConcreteBuilder3" class ScooterBuilder : VehicleBuilder { public ScooterBuilder() { vehicle = new Vehicle("Scooter"); } public override void BuildFrame() { vehicle["frame"] = "Scooter Frame"; } public override void BuildEngine() { vehicle["engine"] = "50 cc"; } public override void BuildWheels() { vehicle["wheels"] = "2"; } public override void BuildDoors() { vehicle["doors"] = "0"; } } // "Product" class Vehicle { private string _vehicleType; private Dictionary<string, string> _parts = new Dictionary<string, string>(); // Constructor public Vehicle(string vehicleType) { this._vehicleType = vehicleType; } // Indexer public string this[string key] { get { return _parts[key]; } set { _parts[key] = value; } } public void Show() { Console.WriteLine("\n---------------------------"); Console.WriteLine("Vehicle Type: {0}", _vehicleType); Console.WriteLine(" Frame : {0}", _parts["frame"]); Console.WriteLine(" Engine : {0}", _parts["engine"]); Console.WriteLine(" #Wheels: {0}", _parts["wheels"]); Console.WriteLine(" #Doors : {0}", _parts["doors"]); } } }
下面为.NET优化的例子,使用枚举来表示车辆不同的组成部分,并通过在子类中调用父类构造函数同一由父类完成对象的构建。产品类中使用了更强类型的Dictionary泛型集合,并使用索引器取代了数组。
// Builder Pattern // .NET Optimized example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Builder.NETOptimized { public class MainApp { public static void Main() { // Create shop var shop = new Shop(); // Construct and display vehicles shop.Construct(new ScooterBuilder()); shop.ShowVehicle(); shop.Construct(new CarBuilder()); shop.ShowVehicle(); shop.Construct(new MotorCycleBuilder()); shop.ShowVehicle(); // Wait for user Console.ReadKey(); } } // "Director" class Shop { private VehicleBuilder _vehicleBuilder; // Builder uses a complex series of steps public void Construct(VehicleBuilder vehicleBuilder) { _vehicleBuilder = vehicleBuilder; _vehicleBuilder.BuildFrame(); _vehicleBuilder.BuildEngine(); _vehicleBuilder.BuildWheels(); _vehicleBuilder.BuildDoors(); } public void ShowVehicle() { _vehicleBuilder.Vehicle.Show(); } } // "Builder" abstract class VehicleBuilder { public Vehicle Vehicle { get; private set; } // Constructor public VehicleBuilder(VehicleType vehicleType) { Vehicle = new Vehicle(vehicleType); } public abstract void BuildFrame(); public abstract void BuildEngine(); public abstract void BuildWheels(); public abstract void BuildDoors(); } // "ConcreteBuilder1" class MotorCycleBuilder : VehicleBuilder { // Invoke base class constructor public MotorCycleBuilder() : base(VehicleType.MotorCycle) { } public override void BuildFrame() { Vehicle[PartType.Frame] = "MotorCycle Frame"; } public override void BuildEngine() { Vehicle[PartType.Engine] = "500 cc"; } public override void BuildWheels() { Vehicle[PartType.Wheel] = "2"; } public override void BuildDoors() { Vehicle[PartType.Door] = "0"; } } // "ConcreteBuilder2" class CarBuilder : VehicleBuilder { // Invoke base class constructor public CarBuilder() : base(VehicleType.Car) { } public override void BuildFrame() { Vehicle[PartType.Frame] = "Car Frame"; } public override void BuildEngine() { Vehicle[PartType.Engine] = "2500 cc"; } public override void BuildWheels() { Vehicle[PartType.Wheel] = "4"; } public override void BuildDoors() { Vehicle[PartType.Door] = "4"; } } // "ConcreteBuilder3" class ScooterBuilder : VehicleBuilder { // Invoke base class constructor public ScooterBuilder() : base(VehicleType.Scooter) { } public override void BuildFrame() { Vehicle[PartType.Frame] = "Scooter Frame"; } public override void BuildEngine() { Vehicle[PartType.Engine] = "50 cc"; } public override void BuildWheels() { Vehicle[PartType.Wheel] = "2"; } public override void BuildDoors() { Vehicle[PartType.Door] = "0"; } } // "Product" class Vehicle { private VehicleType _vehicleType; private Dictionary<PartType, string> _parts = new Dictionary<PartType, string>(); // Constructor public Vehicle(VehicleType vehicleType) { _vehicleType = vehicleType; } public string this[PartType key] { get { return _parts[key]; } set { _parts[key] = value; } } public void Show() { Console.WriteLine("\n---------------------------"); Console.WriteLine("Vehicle Type: {0}", _vehicleType); Console.WriteLine(" Frame : {0}", this[PartType.Frame]); Console.WriteLine(" Engine : {0}", this[PartType.Engine]); Console.WriteLine(" #Wheels: {0}", this[PartType.Wheel]); Console.WriteLine(" #Doors : {0}", this[PartType.Door]); } } // Part type enumeration public enum PartType { Frame, Engine, Wheel, Door } // Vehicle type enumeration public enum VehicleType { Car, Scooter, MotorCycle } }
实现过程图解
在这里我们还是以去KFC店买套餐为例子,示意图如下:
图2. 使用快餐生产流程展示建造者模式
客户端:顾客。想去买一套套餐(这里面包括汉堡,可乐,薯条),可以有1号和2号两种套餐供顾客选择。
指导者角色:收银员。知道顾客想要买什么样的套餐,并告诉餐馆员工去准备套餐。
建造者角色:餐馆员工。按照收银员的要求去准备具体的套餐,分别放入汉堡,可乐,薯条等。
产品角色:最后的套餐,所有的东西放在同一个盘子里面。
下面开始我们的买套餐过程。
1.客户创建Director对象,并用它所想要的Builder对象进行配置。顾客进入KFC店要买套餐,先找到一个收银员,相当于创建了一个指导者对象。这位收银员给出两种套餐供顾客选择:1普通套餐,2黄金套餐。完成的工作如时序图中红色部分所示。
图3. 快餐生产流程示例时序图1
程序实现:
using System; using System.Collections; using System.Configuration; using System.Reflection; namespace KFC { // Client 类 public class Client { public static void Main(string[] args) { FoodManager foodmanager = new FoodManager(); Builder instance; Console.WriteLine("Please Enter Food No:"); string No = Console.ReadLine(); string foodType = ConfigurationSettings.AppSettings["No" + No]; instance = (Builder)Assembly.Load("KFC").CreateInstance("KFC." + foodType); foodmanager.Construct(instance); } } }
产品(套餐)类:
namespace KFC { // Food类,即产品类 public class Food { Hashtable food = new Hashtable(); /// <summary> /// 添加食品 /// </summary> /// <param name="strName">食品名称</param> /// <param name="Price">价格</param> public void Add(string strName, string Price) { food.Add(strName, Price); } // 显示食品清单 public void Show() { IDictionaryEnumerator myEnumerator = food.GetEnumerator(); Console.WriteLine("Food List:"); Console.WriteLine("------------------------------"); string strfoodlist = ""; while (myEnumerator.MoveNext()) { strfoodlist = strfoodlist + "\n\n" + myEnumerator.Key; strfoodlist = strfoodlist + ":\t" + myEnumerator.Value; } Console.WriteLine(strfoodlist); Console.WriteLine("\n------------------------------"); } } }
2.指导者通知建造器。收银员(指导者)告知餐馆员工准备套餐。这里我们准备套餐的顺序是:放入汉堡,可乐倒入杯中,薯条放入盒中,并把这些东西都放在盘子上。这个过程对于普通套餐和黄金套餐来说都是一样的,不同的是它们的汉堡,可乐,薯条价格不同而已。如时序图红色部分所示:
图4. 快餐生产流程示例时序图2
程序实现:
namespace KFC { // FoodManager类,即指导者 public class FoodManager { public void Construct(Builder builder) { builder.BuildHamb(); builder.BuildCoke(); builder.BuildChip(); } } }
3.建造者处理指导者的要求,并将部件添加到产品中。餐馆员工(建造者)按照收银员要求的把对应的汉堡,可乐,薯条放入盘子中。这部分是建造者模式里面富于变化的部分,因为顾客选择的套餐不同,套餐的组装过程也不同,这步完成产品对象的创建工作。
程序实现:
namespace KFC { // Builder类,即抽象建造者类,构造套餐 public abstract class Builder { // 添加汉堡 public abstract void BuildHamb(); // 添加可乐 public abstract void BuildCoke(); // 添加薯条 public abstract void BuildChip(); // 返回结果-食品对象 public abstract Food GetFood(); } } namespace KFC { // NormalBuilder类,具体构造者,普通套餐 public class NormalBuilder : Builder { private Food NormalFood = new Food(); public override void BuildHamb() { NormalFood.Add("NormalHamb", "¥10.50"); } public override void BuildCoke() { NormalFood.Add("CokeCole", "¥4.50"); } public override void BuildChip() { NormalFood.Add("FireChips", "¥2.00"); } public override Food GetFood() { return NormalFood; } } } namespace KFC { // GoldBuilder类,具体构造者,黄金套餐 public class GoldBuilder : Builder { private Food GoldFood = new Food(); public override void BuildHamb() { GoldFood.Add("GoldHamb", "¥13.50"); } public override void BuildCoke() { GoldFood.Add("CokeCole", "¥4.50"); } public override void BuildChip() { GoldFood.Add("FireChips", "¥3.50"); } public override Food GetFood() { return GoldFood; } } }
4.客户从建造者检索产品。从餐馆员工准备好套餐后,顾客再从餐馆员工那儿拿回套餐。这步客户程序要做的仅仅是取回已经生成的产品对象,如时序图中红色部分所示。
图5. 快餐生产流程示例时序图3
完整的客户程序:
namespace KFC { // Client 类 public class Client { public static void Main(string[] args) { FoodManager foodmanager = new FoodManager(); Builder instance; Console.WriteLine("Please Enter Food No:"); string No = Console.ReadLine(); string foodType = ConfigurationSettings.AppSettings["No" + No]; instance = (Builder)Assembly.Load("KFC").CreateInstance("KFC." + foodType); foodmanager.Construct(instance); Food food = instance.GetFood(); food.Show(); Console.ReadLine(); } } }
通过分析不难看出,在这个例子中,在准备套餐的过程是稳定的,即按照一定的步骤去做,而套餐的组成部分则是变化的,有可能是普通套餐或黄金套餐等。这个变化就是建造者模式中的"变化点",就是我们要封装的部分。
另外一个例子
在这里我们再给出另外一个关于建造房子的例子。客户程序通过调用指导者 (Cdirector类)的BuildHouse()方法来创建一个房子。该方法有一个布尔型的参数blnBackyard,当blnBackyard为假时指导者将创建一个Apartment(Concrete Builder),当它为真时将创建一个Single Family Home(Concrete Builder)。这两种房子都实现了接口Ihouse。
程序实现:
//关于建造房屋的例子 using System; using System.Collections; // 抽象建造者 public interface IHouse { bool GetBackyard(); long NoOfRooms(); string Description(); } // 具体建造者 public class CApt : IHouse { private bool mblnBackyard; private Hashtable Rooms; public CApt() { CRoom room; Rooms = new Hashtable(); room = new CRoom(); room.RoomName = "Master Bedroom"; Rooms.Add("room1", room); room = new CRoom(); room.RoomName = "Second Bedroom"; Rooms.Add("room2", room); room = new CRoom(); room.RoomName = "Living Room"; Rooms.Add("room3", room); mblnBackyard = false; } public bool GetBackyard() { return mblnBackyard; } public long NoOfRooms() { return Rooms.Count; } public string Description() { IDictionaryEnumerator myEnumerator = Rooms.GetEnumerator(); string strDescription; strDescription = "This is an Apartment with " + Rooms.Count + " Rooms \n"; strDescription = strDescription + "This Apartment doesn't have a backyard \n"; while (myEnumerator.MoveNext()) { strDescription = strDescription + "\n" + myEnumerator.Key + "\t" + ((CRoom)myEnumerator.Value).RoomName; } return strDescription; } } // 具体建造者 public class CSFH : IHouse { private bool mblnBackyard; private Hashtable Rooms; public CSFH() { CRoom room; Rooms = new Hashtable(); room = new CRoom(); room.RoomName = "Master Bedroom"; Rooms.Add("room1", room); room = new CRoom(); room.RoomName = "Second Bedroom"; Rooms.Add("room2", room); room = new CRoom(); room.RoomName = "Third Room"; Rooms.Add("room3", room); room = new CRoom(); room.RoomName = "Living Room"; Rooms.Add("room4", room); room = new CRoom(); room.RoomName = "Guest Room"; Rooms.Add("room5", room); mblnBackyard = true; } public bool GetBackyard() { return mblnBackyard; } public long NoOfRooms() { return Rooms.Count; } public string Description() { IDictionaryEnumerator myEnumerator = Rooms.GetEnumerator(); string strDescription; strDescription = "This is an Single Family Home with " + Rooms.Count + " Rooms \n"; strDescription = strDescription + "This house has a backyard \n"; while (myEnumerator.MoveNext()) { strDescription = strDescription + "\n" + myEnumerator.Key + "\t" + ((CRoom)myEnumerator.Value).RoomName; } return strDescription; } } public interface IRoom { string RoomName { get; set; } } public class CRoom : IRoom { private string mstrRoomName; public string RoomName { get { return mstrRoomName; } set { mstrRoomName = value; } } } // 指导者 public class CDirector { public IHouse BuildHouse(bool blnBackyard) { if (blnBackyard) { return new CSFH(); } else { return new CApt(); } } } /// <summary> /// 客户程序 /// </summary> public class Client { static void Main(string[] args) { CDirector objDirector = new CDirector(); IHouse objHouse; string Input = Console.ReadLine(); objHouse = objDirector.BuildHouse(bool.Parse(Input)); Console.WriteLine(objHouse.Description()); Console.ReadLine(); } }
建造者模式的几种演化
省略抽象建造者角色
系统中只需要一个具体建造者,省略掉抽象建造者,结构图如下:
图6. 省略抽象建造者角色的建造者模式
指导者代码如下:
class Director { private ConcreteBuilder builder; public void Construct() { builder.BuildPartA(); builder.BuildPartB(); } }
省略指导者角色
抽象建造者角色已经被省略掉,还可以省略掉指导者角色。让Builder角色自己扮演指导者与建造者双重角色。结构图如下:
图7. 省略指导者角色的建造者模式
建造者角色代码如下:
public class Builder { private Product product = new Product(); public void BuildPartA() { // } public void BuildPartB() { // } public Product GetResult() { return product; } public void Construct() { BuildPartA(); BuildPartB(); } }
客户程序:
public class Client { private static Builder builder; public static void Main() { builder = new Builder(); builder.Construct(); Product product = builder.GetResult(); } }
合并建造者角色和产品角色
建造模式失去抽象建造者角色和指导者角色后,可以进一步退化,从而失去具体建造者角色,此时具体建造者角色和产品角色合并,从而使得产品自己就是自己的建造者。这样做混淆了对象的建造者和对象本身,但是有时候一个产品对象有着固定的几个零件,而且永远只有这几个零件,此时将产品类和建造类合并,可以使系统简单易读。结构图如下:
图8. 将指导者与建造者的任务集成于产品类中
实现要点
-
建造者模式主要用于"分步骤构建一个复杂的对象",在这其中"分步骤"是一个稳定的算法,而复杂对象的各个部分则经常变化。
-
产品不需要抽象类,特别是由于创建对象的算法复杂而导致使用此模式的情况下或者此模式应用于产品的生成过程,其最终结果可能差异很大,不大可能提炼出一个抽象产品类。
-
创建者中的创建子部件的接口方法不是抽象方法而是空方法,不进行任何操作,具体的创建者只需要覆盖需要的方法就可以,但是这也不是绝对的,特别是类似文本转换这种情况下,缺省的方法将输入原封不动的输出是合理的缺省操作。
-
前面我们说过的抽象工厂模式(Abstract Factory)解决"系列对象"的需求变化,Builder模式解决"对象部分"的需求变化,建造者模式常和组合模式(Composite Pattern)结合使用。
效果
-
建造者模式的使用使得产品的内部表象可以独立的变化。使用建造者模式可以使客户端不必知道产品内部组成的细节。
-
每一个Builder都相对独立,而与其它的Builder无关。
-
可使对构造过程更加精细控制。
-
将构建代码和表示代码分开。
-
建造者模式的缺点在于难于应付"分步骤构建算法"的需求变动。
-
使用建造者模式,用户就只需指定需要建造的类型就可以得到它们,而具体建造的过程和细节就不需要知道了。
-
建造者模式的好处就是使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需改变一个产品的内部表示,只需要再定义一个具体的建造者就可以了。
应用场景
-
RTF文档交换格式阅读器。
-
.NET环境下的字符串处理StringBuilder,这是一种简化了的建造者模式。
.NET中的应用
.NET Framework中,VBCodeProvider和CSSharpCodeProvider的CreateGenerator方法返回各种实现了ICodeGenerator接口的具体Builder类。这些类被用来控制代码的生成。
总结
建造者模式的实质是解耦组装过程和创建具体部件,使得我们不用去关心每个部件是如何组装的。