相信大多数的人都看过《西游记》,对孙悟空拔毛变出小猴子的故事情节应该都很熟悉。孙悟空能够用猴毛根据本身的形象复制出不少跟本身如出一辙的小猴兵出来,其实在设计模式中也有一个相似的模式,咱们能够经过一个原型对象来克隆出多个如出一辙的对象,这个模式就是原型模式。编程
原型模式(Prototype) | 学习难度:★★★☆☆ | 使用频率:★★★☆☆ |
M公司一直在使用自行开发的一个OA系统进行平常工做办理,但在使用过程当中,愈来愈多的人对工做周报的建立和编写模块产生了抱怨。追其缘由,M公司的OA管理员发现,因为某些岗位每周工做存在重复性,工做周报内容都大同小异,以下图所示:设计模式
这些周报只有一些小地方存在差别,可是现行系统每周默认建立的周报都是空白报表,所以用户只能经过从新输入或不断地复制与粘贴来填写重复的周报内容,极大地下降了工做效率,浪费宝贵的时间。如何快速建立相同或者类似的工做周报,成为了M公司软件开发人员的一个新问题。网络
M公司开发人员通过分析,决定按照如下思路对工做周报模块进行从新设计:ide
(1)除了容许用户建立新周报外,还容许用户将建立好的周报保存为模板(也就是原型)。学习
(2)用户在再次建立周报时,能够建立全新的周报,还能够选择合适的模板复制生成一个相同的周报,而后对新生成的周报根据实际状况进行修改,产生新的周报。测试
原型模式的原理很简单,将一个原型对象传给那个要发动建立的对象,这个要发动建立的对象经过请求原型对象克隆本身来实现建立过程。this
原型模式(Prototype):使用原型实例指定建立对象的种类,而且经过拷贝这些原 型建立新的对象。原型模式是一种对象建立型模式。spa
须要注意的是,经过克隆方法所建立的对象时全新的对象。prototype
原型模式的结构以下图所示:设计
● Prototype(抽象原型类):它是声明克隆方法的接口,是全部具体原型类的公共父类,能够是抽象类也能够是接口,甚至还能够是具体实现类。
● ConcretePrototype(具体原型类):它实如今抽象原型类中声明的克隆方法,在克隆方法中返回本身的一个克隆对象
● Client(客户类):让一个原型对象克隆自身从而建立一个新的对象,在客户类中只须要直接实例化或经过工厂方法等方式建立一个原型对象,再经过调用该对象的克隆方法便可获得多个相同的对象。因为客户类针对抽象原型类Prototype编程,所以用户能够根据须要选择具体原型类,系统具备较好的可扩展性,增长或更换具体原型类都很方便。
(1)通用实现方法
public class ConcretePrototype : Prototype { // 克隆方法 public override Prototype Clone() { // 建立新对象 Prototype prototype = new ConcretePrototype(); prototype.CustomAttr = this.CustomAttr; return prototype; } }
(2)借助C#语言的Clone方法
public class ConcretePrototypeB : ICloneable { public int i = 0; public string customAttr = "hello prototype"; public ConcretePrototype a = new ConcretePrototype(); public object Clone() { // 实现深复制-方式1:依次赋值和实例化 ConcretePrototypeB newObj = new ConcretePrototypeB(); newObj.a = new ConcretePrototype(); newObj.a.CustomAttr = this.a.CustomAttr; newObj.i = this.i; return newObj; } public new object MemberwiseClone() { // 实现浅复制 return base.MemberwiseClone(); } public override string ToString() { string result = string.Format("I的值为{0},A为{1}", this.i.ToString(), this.a.CustomAttr); return result; } }
M公司开发人员决定使用原型模式来实现工做周报的快速建立:
这里,Object至关于抽象原型类,而全部实现了ICloneable接口的类都至关于具体原型类。
(1)WeeklyLog代码
/// <summary> /// 工做周报:具体原型类 /// 考虑到代码可读性和易理解性,只列出部分与原型模式相关的核心代码 /// </summary> public class WeeklyLog : ICloneable { public string Name { get; set; } public string Date { get; set; } public string Content { get; set; } public object Clone() { WeeklyLog obj = new WeeklyLog(); obj.Name = this.Name; obj.Date = this.Date; obj.Content = this.Content; return obj; } }
(2)Client代码
public class Client { public static void PrintWeeklyLog(WeeklyLog log) { if (log == null) { return; } Console.WriteLine("----------- start : M公司我的工做周报 -----------"); Console.WriteLine("周次:{0}", log.Date); Console.WriteLine("员工:{0}", log.Name); Console.WriteLine("内容:{0}", log.Content); Console.WriteLine("----------- end : M公司我的工做周报 -----------"); } public static void V1() { // First version WeeklyLog log = new WeeklyLog(); log.Name = "Victor"; log.Date = "第11周"; log.Content = "这周工做太忙,天天都在加班!~~~~(>_<)~~~~"; PrintWeeklyLog(log); // Second version based on First version WeeklyLog log2 = log.Clone() as WeeklyLog; log2.Date = "第12周"; PrintWeeklyLog(log2); // Third version based on First version WeeklyLog log3 = log.Clone() as WeeklyLog; log3.Date = "第13周"; PrintWeeklyLog(log3); } }
执行结果以下图所示:
通过改进后的工做周报已经得到用户的一致好评,可是,又有员工提出有些周报带有附件,若是使用上面的实现,周报的附件并不可以复制成功。在进入设计以前,咱们先来了解一下浅复制和深复制。
(1)浅复制:复制一个对象的时候,仅仅复制原始对象中全部的非静态类型成员和全部的引用类型成员的引用。(新对象和原对象将共享全部引用类型成员的实际对象)
(2)深复制:复制一个对象的时候,不只复制全部非静态类型成员,还要复制全部引用类型成员的实际对象。
先来看看浅复制的实现:
public class WeeklyLog : ICloneable { public string Name { get; set; } public string Date { get; set; } public string Content { get; set; } public IList<Attachment> attachmentList { get; set; } // v2 public WeeklyLog() { this.attachmentList = new List<Attachment>(); } public object Clone() { // v1 WeeklyLog obj = new WeeklyLog(); obj.Name = this.Name; obj.Date = this.Date; obj.Content = this.Content; // v2 -- shallow copy obj.attachmentList = this.attachmentList; return obj; } }
客户端测试代码:
public static void Main() { // First version WeeklyLog log = new WeeklyLog(); log.attachmentList.Add(new Attachment() { Name = "工做总结20170426-20170501_Victor.xlsx" }); // Second version WeeklyLog log2 = log.Clone() as WeeklyLog; // Compare 2 object Console.WriteLine("周报是否相同:{0}", object.ReferenceEquals(log, log2)); // Compare 2 attachment Console.WriteLine("附件是否相同:{0}", object.ReferenceEquals(log.attachmentList[0], log2.attachmentList[0])); }
因为使用的是浅复制,所以附件对象的内存地址指向的是同一个对象。
再来看看深复制的实现:
[Serializable] public class WeeklyLog : ICloneable { public string Name { get; set; } public string Date { get; set; } public string Content { get; set; } public IList<Attachment> attachmentList { get; set; } // v2,v3 public WeeklyLog() { this.attachmentList = new List<Attachment>(); } public object Clone() { // v1 //WeeklyLog obj = new WeeklyLog(); //obj.Name = this.Name; //obj.Date = this.Date; //obj.Content = this.Content; // v2 -- shallow copy //obj.attachmentList = this.attachmentList; //return obj; // v3 -- deep copy BinaryFormatter bf = new BinaryFormatter(); MemoryStream ms = new MemoryStream(); bf.Serialize(ms, this); ms.Position = 0; return bf.Deserialize(ms); } }
这里借助序列化来实现深复制,所以别忘记给须要深复制的对象的类定义上面加上可序列化的标签[Serializable]。
客户端测试代码:
public static void Main() { // First version WeeklyLog log = new WeeklyLog(); log.attachmentList.Add(new Attachment() { Name = "工做总结20170426-20170501_Victor.xlsx" }); // Second version WeeklyLog log2 = log.Clone() as WeeklyLog; // Compare 2 object Console.WriteLine("周报是否相同:{0}", object.ReferenceEquals(log, log2)); // Compare 2 attachment Console.WriteLine("附件是否相同:{0}", object.ReferenceEquals(log.attachmentList[0], log2.attachmentList[0])); }
此时,借助深复制克隆的对象已经再也不是指向同一个内存地址的了,所以两个附件也是不一样的:
原型管理器(Prototype Manager)将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,若是须要某个原型对象的一个克隆,能够经过复制集合中对应的原型对象来得到。在原型管理器中针对抽象原型类进行编程,以便于扩展。
原型管理器对应的结构图以下:
M公司在平常办公中有许多公文须要建立、递交和审批,好比:《可行性分析报告》、《立项建设书》、《软件需求说明书》等等。为了提升工做效率,在OA系统中为各种公文均建立了模板,用户能够经过这些模板快速建立新的公文,这些公文模板须要统一进行管理,系统根据用户请求的不一样生成不一样的新公文。
开发人员决定使用原型管理器来设计,其结构图以下:
(1)抽象原型与具体原型
public interface OfficeDocument : ICloneable { new OfficeDocument Clone(); // 隐藏ICloneable的Clone接口方法定义 void Display(); } public class FAR : OfficeDocument { public OfficeDocument Clone() { return new FAR(); } public void Display() { Console.WriteLine("<<可行性分析报告>>"); } object ICloneable.Clone() { return this.Clone(); } } public class SRS : OfficeDocument { public OfficeDocument Clone() { return new SRS(); } public void Display() { Console.WriteLine("<<软件需求规格说明书>>"); } object ICloneable.Clone() { return this.Clone(); } }
(2)原型管理器
public class PrototypeManager { private Dictionary<string, OfficeDocument> dictOD = new Dictionary<string, OfficeDocument>(); public static PrototypeManager GetInstance() { return Nested.instance; } class Nested { static Nested() { } internal static readonly PrototypeManager instance = new PrototypeManager(); } private PrototypeManager() { dictOD.Add("FAR", new FAR()); dictOD.Add("SRS", new SRS()); } public void AddOfficeDocument(string key, OfficeDocument doc) { dictOD.Add(key, doc); } public OfficeDocument GetOfficeDocumentByKey(string key) { key = key.ToUpper(); if (!dictOD.ContainsKey(key)) { return null; } return dictOD[key].Clone(); } }
这里PrototypeManager采用了单例模式(有利于节省系统资源),并经过一个Dictionary集合保存原型对象,客户端即可以经过Key来获取对应原型的克隆对象。
(3)客户端代码
public static void Main() { PrototypeManager pm = PrototypeManager.GetInstance(); OfficeDocument doc1, doc2, doc3, doc4; doc1 = pm.GetOfficeDocumentByKey("FAR"); doc1.Display(); doc2 = pm.GetOfficeDocumentByKey("FAR"); doc2.Display(); Console.WriteLine("是不是同一个FAR:{0}", object.ReferenceEquals(doc1, doc2)); doc3 = pm.GetOfficeDocumentByKey("SRS"); doc3.Display(); doc4 = pm.GetOfficeDocumentByKey("SRS"); doc4.Display(); Console.WriteLine("是不是同一个SRS:{0}", object.ReferenceEquals(doc3, doc4)); }
运行结果以下:
(1)当建立新的对象实例较为复杂时,使用原型模式能够简化对象的建立过程,经过复制一个已有的实例能够提升新实例的建立效率。
(2)可使用深复制的方式保存对象的状态。将对象复制一份并将其状态保存起来,以便于在使用的时候使用,好比恢复到某一个历史状态,能够辅助实现撤销操做。
(1)须要为每个类配备一个克隆方法,并且该克隆方法位于一个类的内部,当对已有的类进行改造时,须要修改源代码,违背了开闭原则。
(2)为了支持深复制,当对象之间存在多重嵌套引用关系时,每一层对象都必须支持深复制,实现起来可能比较麻烦。
最主要的应用场景就在于 建立新对象成本较大(例如初始化须要占用较长的时间,占用太多的CPU资源或者网络资源),新的对象能够经过原型模式对已有对象进行复制来得到。若是是类似对象,则能够对其成员变量稍做修改。
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》