原型模式是用原型實例指定建立對象的種類,並經過拷貝這些原型建立新的對象。簡單地說就是,首先建立一個實例,而後經過這個實例去拷貝(克隆)建立新的實例。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
接口的使用。