原型模式是用原型实例指定建立对象的种类,并经过拷贝这些原型建立新的对象。简单地说就是,首先建立一个实例,而后经过这个实例去拷贝(克隆)建立新的实例。git
咱们仍是经过一个简单需求开始提及,一般状况下,找工做时,须要准备多份简历,简历信息大体相同,可是能够根据不一样的公司的岗位需求微调工做经历细节,以及薪资要求,例若有的公司要求电商经验优先,那么就能够把电商相关的工做细节多写一点,而有的要求管理经验,那么工做细节就须要更多的体现管理才能,薪资要求也会根据具体状况填写具体数值或者面议等。github
咱们先抛开原型模式不谈,咱们能够考虑一下,前面讲到的几个建立型模式可否知足需求呢?数组
首先,咱们须要多份简历,单例模式直接就能够Pass掉了,其次,因为简历信息比较复杂,起码也有几十个字段,而且根据不一样状况,可能会发生部分修改,所以,三个工厂模式也不能知足需求。不过想到这里,咱们想到建造者模式或许知足需求,由于它就是用来建立复杂对象的,不妨先用建造者模式试一下。安全
先定义简历:框架
public abstract class ResumeBase { /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 性别 /// </summary> public string Gender { get; set; } /// <summary> /// 年龄 /// </summary> public int Age { get; set; } /// <summary> /// 指望薪资 /// </summary> public string ExpectedSalary { get; set; } public abstract void Display(); } /// <summary> /// 工做经历 /// </summary> public class WorkExperence { public string Company { get; set; } public string Detail { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public void Display() { Console.WriteLine("工做经历:"); Console.WriteLine($"{this.Company}\t{this.StartDate.ToShortDateString()}-{EndDate.ToShortDateString()}"); Console.WriteLine("工做详细:"); Console.WriteLine(this.Detail); } } public class ItResume : ResumeBase { /// <summary> /// 工做经历 /// </summary> public WorkExperence WorkExperence { get; set; } public override void Display() { Console.WriteLine($"姓名:\t{this.Name}"); Console.WriteLine($"性别:\t{this.Gender}"); Console.WriteLine($"年龄:\t{this.Age}"); Console.WriteLine($"指望薪资:\t{this.ExpectedSalary}"); Console.WriteLine("--------------------------------"); if (this.WorkExperence != null) { this.WorkExperence.Display(); } Console.WriteLine("--------------------------------"); } }
再定义建造者:ide
public class BasicInfo { /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 性别 /// </summary> public string Gender { get; set; } /// <summary> /// 年龄 /// </summary> public int Age { get; set; } /// <summary> /// 指望薪资 /// </summary> public string ExpectedSalary { get; set; } } public interface IResumeBuilder { IResumeBuilder BuildBasicInfo(Action<BasicInfo> buildBasicInfoDelegate); IResumeBuilder BuildWorkExperence(Action<WorkExperence> buildWorkExperenceDelegate); ResumeBase Build(); } public class ResumeBuilder : IResumeBuilder { private readonly BasicInfo _basicInfo = new BasicInfo(); private readonly WorkExperence _workExperence = new WorkExperence(); public IResumeBuilder BuildBasicInfo(Action<BasicInfo> buildBasicInfoDelegate) { buildBasicInfoDelegate?.Invoke(_basicInfo); return this; } public IResumeBuilder BuildWorkExperence(Action<WorkExperence> buildWorkExperenceDelegate) { buildWorkExperenceDelegate?.Invoke(_workExperence); return this; } public ResumeBase Build() { ItResume resume = new ItResume() { Name = this._basicInfo.Name, Gender = this._basicInfo.Gender, Age = this._basicInfo.Age, ExpectedSalary = this._basicInfo.ExpectedSalary, WorkExperence = new WorkExperence { Company = this._workExperence.Company, Detail = this._workExperence.Detail, StartDate = this._workExperence.StartDate, EndDate = this._workExperence.EndDate } }; return resume; } }
其中,定义一个BasicInfo
类是为了向外暴漏更少的参数,Build()
方法每次调用都会产生一个全新的ItResume
对象。性能
调用的地方也很是简单:优化
static void Main(string[] args) { IResumeBuilder resumeBuilder = new ResumeBuilder() .BuildBasicInfo(resume => { resume.Name = "张三"; resume.Age = 18; resume.Gender = "男"; resume.ExpectedSalary = "100W"; }) .BuildWorkExperence(work => { work.Company = "A公司"; work.Detail = "负责XX系统开发,精通YY。。。。。"; work.StartDate = DateTime.Parse("2019-1-1"); work.EndDate = DateTime.Parse("2020-1-1"); }); ResumeBase resume1 = resumeBuilder .Build(); ResumeBase resume2 = resumeBuilder .BuildBasicInfo(resume => { resume.ExpectedSalary = "面议"; }) .BuildWorkExperence(work => { work.Detail = "电商经验丰富"; }) .Build(); resume1.Display(); resume2.Display(); }
这样好像就已经知足需求了,咱们只须要少许修改就能够建立多份简历。可是呢,这种状况,每次建立一批简历以前,咱们都必须先有一个Builder,不然没法完成简历的建立,而咱们实际指望的是直接经过一份旧的简历就能够复制获得一份新简历,在这种指望下,并无所谓的Builder存在。
可是经过观察咱们不难发现,旧简历其实已经具有了生产新简历的全部参数,惟一缺乏的就是Build()
方法,所以,既然不能使用Builder,咱们直接将Builder中的Build()
方法Copy
到Resume中不就能够了吗?因而就有了以下改造,将Build()
方法完整的Copy
到ResumeBase
和ItResume
中,仅仅将方法名改为了Clone()
:ui
public abstract class ResumeBase { ... public abstract ResumeBase Clone(); } public class ItResume : ResumeBase { ... public override ResumeBase Clone() { ItResume resume = new ItResume() { Name = this.Name, Gender = this.Gender, Age = this.Age, ExpectedSalary = this.ExpectedSalary, WorkExperence = new WorkExperence { Company = this.WorkExperence.Company, Detail = this.WorkExperence.Detail, StartDate = this.WorkExperence.StartDate, EndDate = this.WorkExperence.EndDate } }; return resume; } }
调用的地方就能够直接经过resume.Clone()
方法建立新的简历了!
完美!其实这就是咱们的原型模式了,仅仅是对建造者模式进行了一点点的改造,就有了神奇的效果!this
咱们再来看一下原型模式的类图:
固然,这种写法还有很大的优化空间,例如,若是对象属性比较多,Clone()
方法的维护就会变得很是麻烦,所以,咱们可使用Object.MemberwiseClone()
来简化调用,以下所示:
public override ResumeBase Clone() { ItResume itResume = this.MemberwiseClone() as ItResume; itResume.WorkExperence = this.WorkExperence.Clone(); return itResume; }
这样就简化不少了,可是又引入了新的问题,MemberwiseClone()
是浅拷贝的,所以要完成深拷贝,就必须全部引用类型的属性都实现Clone()
功能,如WorkExperence
,不然,在后续调用时可能出现因为数据共享而产生的未知错误,这多是灾难性的,由于很难排查出错误出在哪里,所以,咱们更建议使用序列化和反序列化的方式来实现深拷贝,以下所示:
[Serializable] public sealed class ItResume : ResumeBase { ... public override ResumeBase Clone() { using (MemoryStream stream = new MemoryStream()) { BinaryFormatter bf = new BinaryFormatter(); bf.Serialize(stream, this); stream.Position = 0; return bf.Deserialize(stream) as ResumeBase; } } }
这里须要注意的是,所涉及的全部引用类型的属性(字符串除外),都须要打上Serializable
标记,不然会抛出异常(抛出异常比MemberwiseClone()
的什么也不发生要好的多),注意,这里的ItResume
最好标记为sealed
,缘由后续解释。
上面提到了浅拷贝和深拷贝,这里简单解释一下。
Object.MemberwiseClone()
是浅拷贝。浅拷贝和深拷贝是相对的,若是一个对象内部只有基本数据类型,那么浅拷贝和深拷贝是等价的。
ICloneable
接口只有一个Clone()
成员方法,咱们一般会用它充当Prototype
基类来实现原型模式,但我这里要说的是尽可能避免使用ICloneable
,缘由在 《Effective C#:50 Specific Ways to Improve Your C#》 一书中的原则27 有给出,基本思想以下:
ICloneable
接口,而且非Sealed类型,那么它的全部派生类都须要实现Clone方法。不然,用派生类对象调用Clone方法,返回的对象将会是基类Clone方法建立的对象,这就给派生类带来了沉重的负担,所以在非密封类中应该避免实现 ICloneable
接口,但这个不是ICloneable
特有的缺陷,任何一种方式实现原型模式都存在该问题,所以建议将原型模式的实现类设置为密封类。object
,是非类型安全的;ICloneable
被不少人认为是一个糟糕的设计,其余理由以下:
ICloneable
除了标识可被克隆以外,不管做为参数仍是返回值都没有任何意义;.Net Framework
在升级支持泛型至今,都没有添加一个与之对应的ICloneable<T>
泛型接口;ICloneable
接口,可是内部只提供了一个抛出异常的私有实现,例如SqlConnection
。鉴于上述诸多缺点,在实现原型模式时,ICloneable
接口能不用就不要用了,本身定义一个更有意义的方法或许会更好。
原型模式一般用在对象建立复杂或者建立过程须要消耗大量资源的场景。但因为其实现过程当中会存在诸多问题,若是处理不当很容易对使用者形成困扰,所以,应尽可能使用序列化反序列化的方式实现,尽可能将其标记为sealed
,另外,尽可能避免对ICloneable
接口的使用。