技术图文:02 创建型设计模式(下)
创建型设计模式(下)
知识结构:

图1 知识结构
单例模式 – 确保对象的唯一性
Sunny 软件公司承接了一个服务器负载均衡软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。
由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。
如何确保负载均衡器的唯一性是该软件成功的关键。

图2 服务器负载均衡器结构图
LoadBalance
中包含一个存储服务器信息的集合_serverList
,每次在_serverList
中随机选择一台服务器来响应客户端的请求。
实现代码如下:
public class LoadBalance
{private readonly IList<string> _serverList;private readonly Random _random = new Random();private static readonly LoadBalance Instance = new LoadBalance();private LoadBalance(){_serverList = new List<string>();}public static LoadBalance GetLoadBalance(){return Instance;}public void AddServer(string server){if (_serverList.Contains(server) == false){_serverList.Add(server);}}public void RemoveServer(string server){_serverList.Remove(server);}public string GetServer(){int count = _serverList.Count;if (count == 0)return string.Empty;return _serverList[_random.Next(count)];}
}
客户端代码如下:
static void Main(string[] args)
{LoadBalance balance1 = LoadBalance.GetLoadBalance();LoadBalance balance2 = LoadBalance.GetLoadBalance();LoadBalance balance3 = LoadBalance.GetLoadBalance();LoadBalance balance4 = LoadBalance.GetLoadBalance();//判断服务器负载均衡器是否相同 if (object.ReferenceEquals(balance1, balance2)&& object.ReferenceEquals(balance2, balance3)&& object.ReferenceEquals(balance3, balance4)){Console.WriteLine("服务器负载均衡器具有唯一性!");}balance1.AddServer("server 1");balance1.AddServer("server 2");balance1.AddServer("server 3");balance1.AddServer("server 4");//模拟客户端请求的分发 for (int i = 0; i < 10; i++){Console.WriteLine("分发请求至服务器:" + balance1.GetServer());}
}

图3 运行结果
单例模式(Singleton Pattern)
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

图4 单例模式结构图
单例模式 有三个要点:
- 为了防止在外部对其实例化,将其构造函数设计为私有;
- 在单例类内部定义一个
Singleton
类型的静态对象,作为外部共享的唯一实例; - 在单例类的内部实现只生成一个实例,同时它提供一个静态的
GetInstance()
工厂方法,让客户可以访问它的唯一实例;
饿汉式单例类与懒汉式单例类的讨论
- 饿汉式单例类
public class EagerSingleton
{private static readonly EagerSingleton Instance = new EagerSingleton();private EagerSingleton(){;}public static void Test(){//用于验证的方法,如果客户端先调用该方法,//Instance也被实例化。}public static EagerSingleton GetInstance(){return Instance;}
}
- 懒汉式单例类
public class LazySingleton
{private static LazySingleton _instance;private LazySingleton(){;}public static void Test(){//用于验证的方法}public static LazySingleton GetInstance(){if (_instance == null)_instance = new LazySingleton();return _instance;}
}
饿汉式单例模式
当加载类时,初始化静态变量Instance
(调用私有构造函数),创建单例类的唯一实例。如果使用饿汉式单例类,不会出现创建多个单例对象的情况,可确保单例对象的唯一性。但可能带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。懒汉式单例模式
懒汉式单例类在第一次调用GetInstance()
方法时实例化,在类加载时并不自实例化,这种技术称为延迟加载(Lazy Load)技术,即需要的时候再加载实例。但实际运行中可能存在多线程调用GetInstance()
方法的情况,这样不能保证单例对象的唯一性。
我们需要使用lock
关键字,代码如下:
public class LazySingleton
{private static LazySingleton _instance;private static readonly object SynRoot = new object();private LazySingleton(){;}public static void Test(){//用于验证的方法}public static LazySingleton GetInstance(){lock (SynRoot){if (_instance == null)_instance = new LazySingleton();}return _instance;}
}
上述代码虽然解决了线程安全问题,但每次调用GetInstance()
时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。而且,同步的代价必然会一定程度的使程序的并发度降低。我们继续对懒汉式单例类进行改进,代码如下:
public class LazySingleton
{private static LazySingleton _instance;private static readonly object SynRoot = new object();private LazySingleton(){;}public static void Test(){//用于验证的方法}public static LazySingleton GetInstance(){if (_instance == null) //第一重判断{lock (SynRoot) //代码锁定{if (_instance == null) //第二重判断_instance = new LazySingleton();}}return _instance;}
}
以上代码方式称为双重检查锁定(Double-Check Locking)。即:想办法把同步的粒度降低,只在初始化对象的时候进行同步。如果不进行第二重判断,也不能保证单例对象的唯一性。
一种更好的单例实现方法(静态内部类)
- 饿汉式单例类虽然无须考虑多线程访问问题,但不能实现延迟加载,不管将来用不用始终占据内存;
- 懒汉式单例类需要通过“双重检查锁定”等机制进行线程安全控制,影响性能。
有没有一种方法即能克服两者的缺点,又能结合两者的优点呢?
参看下面的代码:
public class Singleton
{private static class HolderClass{public static readonly Singleton Instance = new Singleton();}private Singleton(){;}public static void Test(){//用于验证的方法,如果客户端先调用该方法,//Instance不被实例化。}public static Singleton GetInstance(){return HolderClass.Instance;//这里将导致HolderClass类被初始化}
}
客户端代码如下:
class Program
{static void Main(string[] args){Singleton s1 = Singleton.GetInstance();Singleton s2 = Singleton.GetInstance();if (object.ReferenceEquals(s1, s2)){Console.WriteLine("两个对象是相同的实例。");}}
}

图5 运行结果
由于静态单例对象没有作为Singleton
的静态成员变量,故没被直接实例化。
当第一次调用GetInstance()
方法时,将加载内部静态类HolderClass
,在该内部类中定义了一个static
类型的成员变量Instance
,此时会首先初始化这个成员变量,由 .NET 来保证其线程安全性,确保该成员变量只能初始化一次。
由于GetInstance()
方法没有任何线程锁定,因此不会对其性能造成影响。我们称这种方式为 Initialization on Demand Holder(IoDH) 技术。
扩展案例
编写一个多文档界面 MDI(Multi-Document Interface) 窗体应用程序,在 MDI 子窗体中有一个“工具箱”窗体,这个窗体要么不出现,出现也只能出现一个。

图6 运行结果
程序代码如下:
public partial class FrmToolBox : Form
{private static FrmToolBox _ftb = default(FrmToolBox);private static readonly object SynRoot = new object();private FrmToolBox(){InitializeComponent();}public static FrmToolBox GetInstance(){if (_ftb == null || _ftb.IsDisposed) {//当Close‘工具箱’时,它的实例并没有变为null//而只是Disposed。所以需要增加对IsDisposed属性的判断lock (SynRoot){if (_ftb == null || _ftb.IsDisposed)//双重锁定{_ftb = new FrmToolBox{MdiParent = ActiveForm};}}}return _ftb;}
}public partial class FrmMain : Form
{public FrmMain(){InitializeComponent();}private void FrmMain_Load(object sender, EventArgs e){this.IsMdiContainer = true;}private void ToolStripMenuItem_Click(object sender, EventArgs e){FrmToolBox.GetInstance().Show();}private void ToolStripButton_Click(object sender, EventArgs e){FrmToolBox.GetInstance().Show();}
}
原型模式 – 对象的克隆
Sunny 软件公司一直使用自行开发的一套OA系统进行日常办工,但在使用过程中,越来越多的人对工作周报的创建和编写模块产生抱怨。
追其原因,Sunny 公司的 OA 系统管理员发现,由于某些岗位每周工作存在重复性,工作周报内容大同小异。这些周报只有一些小地方存在差异,但是现行系统每周默认创建的周报都是空白报表,用户只能通过重新输入或不断复制粘贴来填写重复的周报内容,极大降低了工作效率,浪费了宝贵的时间。
如何快速创建相同或者相似的工作周报,成为 Sunny 公司 OA 开发人员面临的一个新问题。
Sunny 公司的开发人员通过对问题进行仔细分析,决定按照如下思路对工作周报进行重新设计和实现:
- 除了允许用户创建新周报外,还允许用户将创建好的周报保存为模版。
- 用户再次创建周报时,即可以创建全新的周报,又可以选择合适的模版复制生成一份相同的周报,然后对新生成的周报根据实际情况进行修改,产生新的周报。
只要按照如上两个步骤进行处理,工作周报的创建效率将得以大大提高。
设计方案如下图所示:

图7 快速创建工作周报结构图
ICloneable
是微软提供的原型接口,它里面只含有一个未实现的克隆方法Clone()
。
代码如下:
public class WeeklyLog : ICloneable
{public string Name { get; set; }public string Date { get; set; }public string Content { get; set; }public object Clone(){return base.MemberwiseClone();}
}
客户端代码如下:
class Program
{static void Print(WeeklyLog weekLog){if (weekLog == null)throw new ArgumentNullException();Console.WriteLine("****周报****");Console.WriteLine("周次:" + weekLog.Date);Console.WriteLine("姓名:" + weekLog.Name);Console.WriteLine("内容:" + weekLog.Content);}static void Main(string[] args){WeeklyLog logPrevious = new WeeklyLog{Name = "Patrick",Date = "第12周",Content = "这周工作很忙。"};Print(logPrevious);WeeklyLog logNew = logPrevious.Clone() as WeeklyLog;if (logNew != null){logNew.Date = "第13周";Print(logNew);}}
}

图8 运行结果
通过已创建的工作周报可以快速创建新的周报,然后在根据需要修改周报,无需从头开始创建。
原型模式 为工作流系统中任务单的快速生成提供了一种解决方案。
原型模式(Prototype Pattern)
使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

图9 原型模式结构图
Prototype
(抽象原型类):它是声明克隆方法的抽象类,是所有实体原型类的父类,可以是抽象类也可以是接口。ConcretePrototype
(实体原型类):它实现抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
在 原型模式 中,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。
需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。
原型模式 的核心在于如何实现克隆方法,下面将介绍两种在 C# 语言中常用的克隆实现方法。
- 通用实现方法
通用的克隆实现方法是在实体原型类的克隆方法中实例化一个与自身类型相同的对象并将相关的参数传入新创建的对象中,保证它们的成员属性相同,最后将其返回。代码如下:
public class ConcretePrototypeA : Prototype
{public string Atrr { get; set; }public override Prototype Clone(){ConcretePrototypeA prototype = new ConcretePrototypeA();prototype.Atrr = Atrr;return prototype;}
}
- C# 提供的克隆方法
所有 C# 类都继承自object
类。而object
类提供了一个protected
的MemberwiseClone()
方法,将一个 C# 对象复制一份。因此在 C# 中可以直接使用该方法来实现对象的克隆。
代码如下:
public class ConcretePrototypeA : Prototype
{public string Atrr { get; set; }public override Prototype Clone(){return MemberwiseClone() as Prototype;}
}
客户端代码如下:
class Program
{static void Main(string[] args){ConcretePrototypeA p1 = new ConcretePrototypeA();ConcretePrototypeA p2 = p1.Clone() as ConcretePrototypeA;ConcretePrototypeA p3 = p1;if (object.ReferenceEquals(p1, p2))Console.WriteLine("p1 equals to p2");if (object.ReferenceEquals(p1, p3))Console.WriteLine("p1 equals to p3");}
}

图10 运行结果
浅克隆与深克隆
通过引入 原型模式,Sunny 公司 OA 系统支持工作周报的快速克隆,提高了工作周报的编写效率。但有员工又发现一个问题,有些工作周报带附件,比如附有:
- 本周项目进展报告汇总表;
- 本周客户反馈信息汇总表等;
- …
如果使用上述 原型模式 来克隆周报,周报虽然可以克隆,但是周报的附件并没有被克隆。
这是由于什么原因导致的呢?如何才能实现周报和附件的同时克隆呢?
在回答这些问题之前,先介绍两种不同的克隆方法:
- 浅克隆(Shallow Clone);
- 深克隆(Deep Clone);
在C#语言中,数据类型分为:
- 值类型(int,double,byte,bool,char…);
- 引用类型(interface,class,array…);
浅克隆和深克隆的主要区别在于是否支持对引用类型成员变量的复制,下面将对两者进行详细介绍。
【1】浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
在 C# 语言中,object
提供的MemberwiseClone()
方法实现的就是浅克隆。

图11 带附件的周报结构(浅克隆)
public class Attachment
{public string Name { get; set; }public void Download(){Console.WriteLine("下载附件,附件名为" + Name);}
}public class WeeklyLog : ICloneable
{public string Name { get; set; }public string Date { get; set; }public string Content { get; set; }public Attachment Attachment { get; set; }public object Clone(){return base.MemberwiseClone();}
}
客户端代码如下:
class Program
{static void Main(string[] args){Attachment attachment = new Attachment();WeeklyLog logPrevious = new WeeklyLog{Attachment = attachment};WeeklyLog logNew = logPrevious.Clone() as WeeklyLog;Console.WriteLine("周报是否相同:");Console.WriteLine(object.ReferenceEquals(logPrevious, logNew));Console.WriteLine("附件对象是否相同:");Console.WriteLine(logNew != null && object.ReferenceEquals(logPrevious.Attachment,logNew.Attachment));}
}

图12 输出结果
【2】深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象。
在 C# 语言中,可以通过序列化的方式来实现深克隆。
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原有对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。注意涉及到的类需要加可被序列化的标签[Serializable]
。
代码如下:
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;[Serializable]
public class Attachment
{public string Name { get; set; }public void Download(){Console.WriteLine("下载附件,附件名为" + Name);}
}[Serializable]
public class WeeklyLog : ICloneable
{public string Name { get; set; }public string Date { get; set; }public string Content { get; set; }public Attachment Attachment { get; set; }public object Clone(){MemoryStream ms = new MemoryStream();BinaryFormatter bf = new BinaryFormatter();bf.Serialize(ms, this);ms.Position = 0;return bf.Deserialize(ms);}
}
客户端代码如下:
class Program
{static void Main(string[] args){Attachment attachment = new Attachment();WeeklyLog logPrevious = new WeeklyLog{Attachment = attachment};WeeklyLog logNew = logPrevious.Clone() as WeeklyLog;Console.WriteLine("周报是否相同:");Console.WriteLine(object.ReferenceEquals(logPrevious, logNew));Console.WriteLine("附件对象是否相同:");Console.WriteLine(logNew != null && object.ReferenceEquals(logPrevious.Attachment, logNew.Attachment));}
}

图13 输出结果
原型管理器的引入和实现
原型管理器(Prototype Manager)是将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。
注意:在原型管理器中要针对抽象原型类进行编程,以便扩展。
Sunny 软件公司在日常办公中有许多公文需要创建、递交和审批,例如:
- 《可行性分析报告》
- 《立项建议书》
- 《软件需求规格说明书》
- 《项目进展报告》
- …
为了提高工作效率,在 OA 系统中各类公文均创建了模版,用户可以通过这些模版快速创建新的公文,故这些公文模版需要统一进行管理,系统根据用户请求的不同生成不同的新公文。

图14 公文管理器结构图
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Collections;[Serializable]
public abstract class OfficialDocument : ICloneable
{public object Clone(){MemoryStream ms = new MemoryStream();BinaryFormatter bf = new BinaryFormatter();bf.Serialize(ms, this);ms.Position = 0;return bf.Deserialize(ms);}public abstract void Display();
}[Serializable]
public class SRS : OfficialDocument
{public override void Display(){Console.WriteLine("软件需求规格说明书(Software Requirements Specification)");}
}[Serializable]
public class FAR : OfficialDocument
{public override void Display(){Console.WriteLine("可行性分析报告(Feasibility Analysis Report)");}
}public class PrototypeManager
{private readonly Hashtable _hashtable = new Hashtable();private static PrototypeManager pm = new PrototypeManager();private PrototypeManager(){_hashtable.Add("FAR", new FAR());_hashtable.Add("SRS", new SRS());}public void AddOfficialDocument(string key, OfficialDocument doc){if (_hashtable.Contains(key) == false){_hashtable.Add(key, doc);}}public OfficialDocument GetOfficialDocument(string key){OfficialDocument od = _hashtable[key] as OfficialDocument;if (od == null)throw new NullReferenceException();return od.Clone() as OfficialDocument;}public static PrototypeManager CreatePrototypeManager(){return pm;}
}
客户端代码如下:
class Program
{static void Main(string[] args){PrototypeManager pm = PrototypeManager.CreatePrototypeManager();OfficialDocument dc1 = pm.GetOfficialDocument("FAR");dc1.Display();OfficialDocument dc2 = pm.GetOfficialDocument("FAR");dc2.Display();Console.WriteLine(object.ReferenceEquals(dc1, dc2));OfficialDocument dc3 = pm.GetOfficialDocument("SRS");dc3.Display();OfficialDocument dc4 = pm.GetOfficialDocument("SRS");dc4.Display();Console.WriteLine(object.ReferenceEquals(dc3, dc4));}
}

图15 输出结果
在本实例代码中,我们将PrototypeManager
设计为饿汉式单例类,确保系统中有且仅有一个PrototypeManager
对象,有利于节省系统资源,并可以更好地对原型管理器对象进行控制。
建造者模式 – 复杂对象的组装与创建
Sunny 软件公司游戏开发小组决定开发一款网络游戏,该游戏采用 RPG(Role Playing Game)模式,玩家可以在游戏中扮演虚拟世界中的一个特定角色,角色根据不同的游戏情节和统计数据(如力量、魔法、技能等)具有不同的能力,角色也会随着不断升级而拥有更强大的能力。
作为 RPG 游戏的一个重要组成部分,需要对游戏角色进行设计,而且随着该游戏的升级将不断增加新的角色。不同类型的游戏角色,其性别、脸型、服装、发型等外部特性都有所差异,例如:
- “天使”拥有美丽的面容和披肩的长发,并身穿一袭白裙;
- “恶魔”极其丑陋,留着光头并穿一件刺眼的黑衣。
Sunny 公司决定开发一个小工具来创建游戏角色,可以创建不同类型的角色并可以灵活增加新的角色。

图16 几种不同的角色对象
Sunny 公司的开发人员通过分析发现,游戏角色是一个复杂对象,它包含性别、脸型等多个组成部分,不同的游戏角色其组成部分有所差异。但无论是何种造型的游戏角色,它的创建步骤都大同小异,都可以按照一定的流程逐步创建其组成部分,再将各组成部分装配成一个完整的游戏角色。
如何一步步创建一个包含多个组成部分的复杂对象,这是一个需要解决的问题。

图17 游戏角色创建结构图
复杂产品代码如下:
//Actor角色类:复杂产品,考虑到代码的可读性,
//只列出部分成员属性,且成员属性的类型均为string,
//真实情况下,有些成员属性的类型需自定义
public class Actor
{public string Type { get; set; }public string Sex { get; set; }public string Face { get; set; }public string Costume { get; set; }public string HairStyle { get; set; }
}
建造者等级结构如下:
public abstract class ActorBuilder
{protected Actor Actor = new Actor();public abstract void BuildType();public abstract void BuildSex();public abstract void BuildFace();public abstract void BuildCostume();public abstract void BuildHairStyle();public Actor CreateActor(){return Actor;}
}public class HeroBuilder : ActorBuilder
{public override void BuildType(){Actor.Type = "英雄";}public override void BuildSex(){Actor.Sex = "男";}public override void BuildFace(){Actor.Face = "英俊";}public override void BuildCostume(){Actor.Costume = "盔甲";}public override void BuildHairStyle(){Actor.HairStyle = "飘逸";}
}public class AngelBuilder : ActorBuilder
{public override void BuildType(){Actor.Type = "天使";}public override void BuildSex(){Actor.Sex = "女";}public override void BuildFace(){Actor.Face = "漂亮";}public override void BuildCostume(){Actor.Costume = "白裙";}public override void BuildHairStyle(){Actor.HairStyle = "披肩长发";}
}public class DevilBuilder : ActorBuilder
{public override void BuildType(){Actor.Type = "恶魔";}public override void BuildSex(){Actor.Sex = "妖";}public override void BuildFace(){Actor.Face = "丑陋";}public override void BuildCostume(){Actor.Costume = "黑衣";}public override void BuildHairStyle(){Actor.HairStyle = "光头";}
}
指挥者代码如下:
public class ActorController
{public Actor Construct(ActorBuilder ab){ab.BuildType();ab.BuildSex();ab.BuildFace();ab.BuildCostume();ab.BuildHairStyle();return ab.CreateActor();}
}
配置文件如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration><appSettings><add key="ClassName" value="SunnyBuilder.DevilBuilder"/></appSettings>
</configuration>
客户端代码如下:
using System.Configuration;
using System.Reflection;
class Program
{static void Main(string[] args){string className = ConfigurationManager.AppSettings["ClassName"];Assembly assembly = Assembly.Load("SunnyBuilder");ActorBuilder actorBuilder = assembly.CreateInstance(className) as ActorBuilder;if (actorBuilder != null){Actor actor = new ActorController().Construct(actorBuilder);Console.WriteLine("类型:"+actor.Type);Console.WriteLine("性别:" + actor.Sex);Console.WriteLine("面容:" + actor.Face);Console.WriteLine("服装:" + actor.Costume);Console.WriteLine("发型:" + actor.HairStyle);}}
}

图18 输出结果
在游戏角色实例中,如果需要更换角色,只需要修改配置文件,更换实体角色创建类即可。也可以增加一个新的实体角色创建类作为抽象角色创建类的子类,再修改配置文件即可,原有代码无须修改,完全符合“开闭原则”。
建造者模式(Builder Pattern)
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式 将客户端与复杂对象(包含多个成员属性的对象)的创建过程分离,客户端无须知道复杂对象的内部装配方式,只需要知道所需建造者的类型即可。
创建者会关注如何一步一步创建一个复杂对象,不同的实体建造者定义了不同的创建过程,且实体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,满足“开闭原则”。

图19 建造者模式结构图
Product
产品类,它是被构建的复杂对象,包含多个部件。Builder
抽象建造者类,它为创建Product
对象的各个部件定义抽象接口。在该接口中一般声明两类方法:BuildPartX()
,用于创建复杂对象的各个部件;GetResult()
,用于返回复杂对象;
ConcreteBuilder1
、ConcreteBuilder2
实体建造者类,复写抽象类Builder
中的抽象方法。Director
指挥者类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在Construct()
方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。
复杂对象代码:
public class Product
{//定义部件,部件可以是任意类型,包括值类型和引用类型public string PartA { get; set; }public string PartB { get; set; }public string PartC { get; set; }
}
建造者等级结构代码:
public abstract class Builder
{protected Product Product = new Product();public abstract void BuildPartA();public abstract void BuildPartB();public abstract void BuildPartC();public Product GetResult(){return Product;}
}public class ConcreteBuilder1 : Builder
{public override void BuildPartA(){Product.PartA = "部件A";}public override void BuildPartB(){Product.PartB = "部件B";}public override void BuildPartC(){Product.PartC = "部件C";}
}public class ConcreteBuilder2 : Builder
{public override void BuildPartA(){Product.PartA = "部件X";}public override void BuildPartB(){Product.PartB = "部件Y";}public override void BuildPartC(){Product.PartC = "部件Z";}
}
指挥者代码如下:
public class Director
{private readonly Builder _builder;public Director(Builder builder){_builder = builder;}public Product Construct(){_builder.BuildPartA();_builder.BuildPartB();_builder.BuildPartC();return _builder.GetResult();}
}
客户端代码如下:
class Program
{static void Main(string[] args){//可通过配置文件实现Builder buidler = new ConcreteBuilder2(); Director director = new Director(buidler);Product p = director.Construct();string str = p.PartA + " " + p.PartB + " " + p.PartC;Console.WriteLine(str);}
}

图20 输出结果
建造者模式 与 抽象工厂模式 有点类似:
- 抽象工厂模式 返回一系列相关产品,而 建造者模式 返回一个完整的复杂产品。
- 在 抽象工厂模式 中,客户端通过选择具体工厂来生成所需对象,而在 建造者模式 中,客户端通过指定具体建造者类型并通过
Director
类去一步一步构建一个复杂对象,然后将结果返回。 - 如果将 抽象工厂模式 看成一个汽车配件生产厂,生产不同类型的汽车配件,那么 建造者模式 就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。
扩展案例
要求用程序画一个在游戏程序里非常常见的小人,现在简单一点,只要求小人有头、身体、两手、两脚就可以了。

图21 输出结果

图22 创建小人结构图
建造者等级结构代码:
public abstract class PersonBuilder
{protected Graphics G;protected Pen P;protected PersonBuilder(Graphics g,Pen p){G = g;P = p;}public abstract void BuildHead();public abstract void BuildBody();public abstract void BuildArmLeft();public abstract void BuildArmRight();public abstract void BuildLegLeft();public abstract void BuildLegRight();
}public class PersonFatBuilder : PersonBuilder
{public PersonFatBuilder(Graphics g, Pen p) : base(g, p){}public override void BuildHead(){G.DrawEllipse(P, 50, 20, 30, 30);//头}public override void BuildBody(){G.DrawRectangle(P, 45, 50, 40, 50);//身体}public override void BuildArmLeft(){G.DrawLine(P, 50, 50, 30, 100);//左手}public override void BuildArmRight(){G.DrawLine(P, 80, 50, 100, 100);//右手}public override void BuildLegLeft(){G.DrawLine(P, 60, 100, 45, 150);//左脚}public override void BuildLegRight(){G.DrawLine(P, 70, 100, 85, 150);//右脚}
}public class PersonThinBuilder : PersonBuilder
{public PersonThinBuilder(Graphics g, Pen p) : base(g, p){}public override void BuildHead(){G.DrawEllipse(P, 50, 20, 30, 30);//头}public override void BuildBody(){G.DrawRectangle(P, 60, 50, 10, 50);//身体}public override void BuildArmLeft(){G.DrawLine(P, 60, 50, 40, 100);//左手}public override void BuildArmRight(){G.DrawLine(P, 70, 50, 90, 100);//右手}public override void BuildLegLeft(){G.DrawLine(P, 60, 100, 45, 150);//左脚}public override void BuildLegRight(){G.DrawLine(P, 70, 100, 85, 150);//右脚}
}
指挥者代码:
public class PersonDirector
{private readonly PersonBuilder _pb;public PersonDirector(PersonBuilder pb){_pb = pb;}public void CreatePerson(){_pb.BuildHead();_pb.BuildBody();_pb.BuildArmLeft();_pb.BuildArmRight();_pb.BuildLegLeft();_pb.BuildLegRight();}
}
客户端代码:
public partial class FrmMain : Form
{public FrmMain(){InitializeComponent();}private void btnThin_Click(object sender, EventArgs e){Graphics gThin = pictureBox1.CreateGraphics();Pen p = new Pen(Brushes.Red);PersonBuilder pb = new PersonThinBuilder(gThin, p);PersonDirector pd = new PersonDirector(pb);pd.CreatePerson();}private void btnFat_Click(object sender, EventArgs e){Graphics gFat = pictureBox2.CreateGraphics();Pen p = new Pen(Brushes.Red);PersonBuilder pb = new PersonFatBuilder(gFat, p);PersonDirector pd = new PersonDirector(pb);pd.CreatePerson();}
}

图23 输出结果
关于Director的进一步讨论
指挥者类Director
在 建造者模式 中扮演非常重要的作用,该类用于指导具体建造者如何构建产品,它按一定次序调用Builder
的BuildPartX()
方法,控制调用的先后次序,并向客户端返回一个完整的产品对象。
下面我们讨论几种Director
的高级应用方式:
【1】省略Director
在有些情况下,为了简化系统结构,可以将Director
和抽象建造者Builder
进行合并,在Builder
中提供逐步构建复杂产品对象的Construct()
方法。
如果将游戏角色设计中的指挥者类ActorController
省略,ActorBuilder
类的代码如下:
public abstract class ActorBuilder
{protected Actor Actor = new Actor();public abstract void BuildType();public abstract void BuildSex();public abstract void BuildFace();public abstract void BuildCostume();public abstract void BuildHairStyle();public Actor Construct(){BuildCostume();BuildFace();BuildHairStyle();BuildSex();BuildType();return Actor;}
}
客户端代码:
using System.Configuration;
using System.Reflection;
class Program
{static void Main(string[] args){string className = ConfigurationManager.AppSettings["ClassName"];Assembly assembly = Assembly.Load("SunnyBuilder");ActorBuilder actorBuilder = assembly.CreateInstance(className) as ActorBuilder;if (actorBuilder != null){Actor actor = actorBuilder.Construct();Console.WriteLine("类型:" + actor.Type);Console.WriteLine("性别:" + actor.Sex);Console.WriteLine("面容:" + actor.Face);Console.WriteLine("服装:" + actor.Costume);Console.WriteLine("发型:" + actor.HairStyle);}}
}
以上对Director
类的省略方式不会影响系统的灵活性和可扩展性,同时还简化了系统结构,但加重了抽象建造者类的职责,如果Construct()
方法较为复杂,待构建产品的组成部分较多,建议还是将Construct()
方法单独封装在Director
中,这样做更符合“单一职责原则”。
【2】钩子方法的引入
建造者模式 除了逐步构建一个复杂产品对象外,还可以通过Director
类来更加精细地控制产品的创建过程。
- 例如,增加钩子方法来控制是否对某个
BuildPartX()
的调用。 - 例如,我们可以在游戏角色的抽象建造者类
ActorBuilder
中定义一个方法IsBareHeaded()
,用于判断某个角色是否为“光头”,默认返回值为false
。
钩子方法的返回类型通常为bool
类型,定义在抽象建造者类中。
建造者等级结构代码:
public abstract class ActorBuilder
{protected Actor Actor = new Actor();public virtual bool IsBareHeaded(){return false;}public abstract void BuildType();public abstract void BuildSex();public abstract void BuildFace();public abstract void BuildCostume();public abstract void BuildHairStyle();public Actor CreateActor(){return Actor;}
}public class DevilBuilder : ActorBuilder
{public override void BuildType(){Actor.Type = "恶魔";}public override void BuildSex(){Actor.Sex = "妖";}public override void BuildFace(){Actor.Face = "丑陋";}public override void BuildCostume(){Actor.Costume = "黑衣";}public override void BuildHairStyle(){Actor.HairStyle = "光头";}public override bool IsBareHeaded(){return true;}
}
指挥者代码:
public class ActorController
{public Actor Construct(ActorBuilder ab){ab.BuildType();ab.BuildSex();ab.BuildFace();ab.BuildCostume();if (ab.IsBareHeaded() == false){ab.BuildHairStyle();}return ab.CreateActor();}
}
后台回复「搜搜搜」,随机获取电子资源!
欢迎关注,请扫描二维码:
相关文章:

[转载]C# 二进制与十进制,十进制与十六进制相互转换
原文地址:C# 二进制与十进制,十进制与十六进制相互转换作者:tonytonglx十进制转二进制:用2辗转相除至结果为1 将余数和最后的1从下向上倒序写就是结果例如302302/2 151 余0151/2 75 余175/2 37 余137/2 18 余118/2 9 余09/2 4 余14/2 …

感知哈希算法——找出相似的图片
参考Neal Krawetz博士的这篇文章, 实现这种功能的关键技术叫做"感知哈希算法"(Perceptual Hash Algorithm), 意思是为图片生成一个指纹(字符串格式), 两张图片的指纹越相似, 说明两张图片就越相似. 但关键是如何根据图片计算出"指纹"呢? 下面用最简单的步…

学web前端需要了解哪些常识
想要学好web前端技术,那么一定要掌握足够的知识,web前端技术包含很多方面的知识,具体学web前端需要了解哪些常识?来看看下面的详细介绍。 学web前端需要了解哪些常识? html css javascript。 要学的内容实在很多,如果没有其他编…

linux下后台执行shell脚本
一句话 nohup sh startup_Server.sh & 转载于:https://www.cnblogs.com/phpcode/archive/2012/04/24/2522761.html

线性代数:第一章 线性方程组
本讲义是自己上课所用幻灯片,里面没有详细的推导过程(笔者板书推导)只以大纲的方式来展示课上的内容,以方便大家下来复习。 从本章开始,我们一起来学习线性代数的有关知识,线性代数的应用之一就是求解复杂…

菜鸟也来学习ORACLE(1)_linux下安装oracle 11g
加入 oracle Club 之前,学长给我们开了个小会 说是看看我们加入的意愿,哎哎 其实直无聊,但是大体比较重视linux 服务器的搭建 以及在linux 下安装oracle 搭建一个oracle 环境吧、我就想这东西能有多难,于是回来就搭建起了&#x…

CSS浮动元素特点有什么
什么是浮动? 元素的浮动是指设置了浮动属性(flot)的元素。 CSS浮动有什么作用? 1.让多个盒子水平排列成一行,浮动成为布局的重要手段; 2.可以实现盒子的左右对齐等等; 3.浮动最早是用来控制图片,实现文字环绕图片的效果。 CSS浮动的语法: 选…

数据结构与算法:11 Leetcode同步练习(四)
目录 题目01:最小栈题目02:有效的括号题目03:用队列实现栈题目04:整数反转题目05:逆波兰表达式求值题目06:全排列题目07:字符串转换整数 (atoi)题目08:设计循环双端队列题目09&…
trie树 详解
前几天学习了并查集和trie树,这里总结一下trie。 本文讨论一棵最简单的trie树,基于英文26个字母组成的字符串,讨论插入字符串、判断前缀是否存在、查找字符串等基本操作;至于trie树的删除单个节点实在是少见,故在此…

启动hadoop的节点
1.启动hadoop的节点 start-dfs.sh 本文转自 素颜猪 51CTO博客,原文链接:http://blog.51cto.com/suyanzhu/1959242

什么是Python线程?Python线程如何创建?
相信正在学习Python技术或者对Python语言有一定了解的人对于Python线程应该都不陌生,但是也有刚接触Python的小伙伴对于Python线程并不了解,今天小编就跟大家聊聊什么是Python线程,又该如何创建Python线程! 什么是Python线程?Python线程如何…

ItemAdding实现数据验证--中文字段,properties.AfterProperties值为null的问题
最近写事件接收器,发现中文字段如果直接用properties.AfterProperties[“申请人"]这样获取的值为null,无法得到值。后拉忽然发现用英文字段可以得到值。难道中文字段需要编码?经过测试果真如此。 代码部分如下:public overri…

jstl c:choose、c:when和c:otherwise标签
在用spring mvc中,页面前端老用jstl,记录一下。 <c:choose>、<c:when>和<c:otherwise>在一起连用,可以实现Java语言中的if-else语句的功能。例如以下代码根据username请求参数的值来打印不同的结果: <c:choo…

怎样设计出优秀的测试用例?看看下面就知道了
想要成为一名合格的软件测试工程师,一份合格软件测试报告是非常重要的,软件测试的核心也就是测试的用例了,我们通过用例可以看出怎么设计出来可以发现问题,可以有效的覆盖需求的,没有冗余的用例是每个测试工程师必须跨…

数据结构与算法:12 数组与稀疏矩阵
12 数组与稀疏矩阵 知识结构: 1. 数组 1.1 数组的定义 数组是具有一定顺序关系的若干对象组成的集合,组成数组的对象称为数组元素。 例如: 向量对应一维数组 A(a0,a1,⋯,an−1)A(a_0,a_1,\cdots,a_{n-1}) A(a0,a1,⋯,an−1) 矩阵…

管理索引表:深入研究B树索引--重建,合并,删除(理论篇3)
重建索引 如果表中记录频繁地被删除或插入,尽管表中的记录总量保持不变,索引空间的使用量会不断增加。虽然记录从索引中被删除,但是该记录索引项的使用空间不能被重新使用。因此,如果表变化不定,索引空间量会不断增…

模块架构不是软件成功的“决定因素”
【本文是09年的一篇旧文,出于某些原因,对原文内容有删减,在这里整理后重新发表】 前言感谢XXX对我们技术,对我们公司产品提出这些意见,我们公司卖的是软件产品,开发软件是一件技术活,说实话&…

JavaScript面向对象修改标签页详解
双击标签页组件中的li小标签或者section 中的文本,可以对文本进行编辑。为了实现这个功能,需要先给li和section元素绑定双击事件,当双击文本后,将文本改成一个文本框,用来输入新的内容,在文本框中显示原来的…

数据结构与算法:13 字符串与整数集合
13 字符串与整数集合 知识点: 1. 字符串 我们古人没有电影电视,没有游戏网络,所以文人们就会想出一些文字游戏来娱乐。比如宋代的李禺写了这样一首诗:“枯眼望遥山隔水,往来曾见几心知?壶空怕酌一杯酒&am…

是时候开始使用JavaScript严格模式了怎样启用javascri
E是时候开始使用JavaScript严格模式了怎样启用javascriCMAScript5将严格模式(strictmode)引入了Javascript中,目的是允许开发人员能够选择“更好”的Javascript版本,这个版本能用不同的方式处理那些普遍而又臭名昭著的错误。一开始的时候,我对…

Linux服务器日志备份到本地
1、确定线上服务器的日志文件名称和路径 2、一台本地服务器能连接公网,创建一个日志账户,设置密码 3、线上服务器要求: a、确定是否已安装sshpass包 [rootiZwz9ghdadtaey1msor7gnZ sh]# rpm -qa|grep sshpass sshpass-1.06-1.el7.x86_64 如不…

学习UI设计能做什么
UI设计这个岗位对于目前的很多企业来说是供不应求的,很多刚培训完UI设计的小伙伴,都不知道该如何定位自己的职能岗,那么学习UI设计能做什么呢?来看看下面小编的详细介绍就知道了。 学习UI设计能做什么? 1、图形设计/界面设计 软件产品的产品…

数据结构与算法:14 Leetcode同步练习(五)
Leetcode同步练习(五) 目录 题目01:用栈实现队列题目02:托普利茨矩阵题目03:罗马数字转整数题目04:最长公共前缀题目05:反转字符串题目06:无重复字符的最长子串题目07:…

Oracle Spatial构建自定义投影坐标系
之前项目换过服务器,移植数据库时候并没有正确完整的移植自定义的投影坐标系,结果就报出莫名其妙的一些错误,比如unable to transform rectangle due to: ORA-13199: SRID does not exist。 因为在移植坐标系的时候仅仅只是将MDSYS.SDO_CRS_C…

php.ini 中开启短标签
控制参数: short_open_tag On如果设置为Off,则不能正常解析类似于这样形式的php文件:<?phpinfo()?>而只能解析<?phpphpinfo()?>这样形式的php文件所以要想php支持短标签,需要我们把short_open_tag 设置为On. 本…

参加UI培训就业多长时间
UI设计在近几年的发展前景是非常好的,越来越多的人都想要学习UI设计,目前大家比较想了解的是参加UI培训就业多长时间?来看看下面的详细介绍。 参加UI培训就业多长时间? 如今市面上的UI设计培训机构很多,选择一个口碑好靠谱的培训机构学习…

数据结构与算法:15 树
15 树 知识结构: 1. 树的基本概念与术语 1.1 树的定义 树是N(N≥0)N(N \geq 0)N(N≥0)个结点组成的有穷集合 ,该集合具有如下特征: (1)除N0N0N0的树外,有且仅有一个特定的称为根的结点。 (…

【as3】键盘事件
在AS3中,键盘事件是由KeyboardEvent类来处理的,属于flash.events包里面,有两种类型的键盘事件:KeyboardEvent.KEY_DOWN 和 KeyboardEvent.KEY_UP,对于键的代码获得我们通过keyCode这个属性 其实键盘事件使用起来还是相…

在后台代码中引入XAML的方法
本文将介绍三种方法用于在后台代码中动态加载XAML,其中有两种方法是加载已存在的XAML文件,一种方法是将包含XAML代码的字符串转换为WPF的对象。 这些是我在编写RegeX时获得的经验,它们将会给WPF程序带来更多的灵活性。 一、在资源字典中载入项…