创建型设计模式(下)
知识结构:
图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();
}
}
后台回复「搜搜搜」,随机获取电子资源!
欢迎关注,请扫描二维码: