ceph-簡介及安裝(luminous)版
定義
原型模式是用原型例項指定建立物件的種類,並通過拷貝這些原型建立新的物件。簡單地說就是,首先建立一個例項,然後通過這個例項去拷貝(克隆)建立新的例項。
需求
我們還是通過一個簡單需求開始說起,通常情況下,找工作時,需要準備多份簡歷,簡歷資訊大致相同,但是可以根據不同的公司的崗位需求微調工作經歷細節,以及薪資要求,例如有的公司要求電商經驗優先,那麼就可以把電商相關的工作細節多寫一點,而有的要求管理經驗,那麼工作細節就需要更多的體現管理才能,薪資要求也會根據具體情況填寫具體數值或者面議等。
我們先拋開原型模式不談,我們可以考慮一下,前面講到的幾個建立型模式能否滿足需求呢?
首先,我們需要多份簡歷,單例模式直接就可以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("--------------------------------"); } }
再定義建造者:
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()
:
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()
方法建立新的簡歷了!
完美!其實這就是我們的原型模式了,僅僅是對建造者模式進行了一點點的改造,就有了神奇的效果!
UML類圖
我們再來看一下原型模式的類圖:
改進
當然,這種寫法還有很大的優化空間,例如,如果物件屬性比較多,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介面
ICloneable
介面只有一個Clone()
成員方法,我們通常會用它充當Prototype
基類來實現原型模式,但我這裡要說的是儘量避免使用ICloneable
,原因在 《Effective C#:50 Specific Ways to Improve Your C#》 一書中的原則27 有給出,基本思想如下:
- 由於只有一個Clone方法,因此呼叫者無法區分到底是深拷貝還是淺拷貝,會給呼叫者造成極大的困擾;
- 如果基類繼承了
ICloneable
介面,並且非Sealed型別,那麼它的所有派生類都需要實現Clone方法。否則,用派生類物件呼叫Clone方法,返回的物件將會是基類Clone方法建立的物件,這就給派生類帶來了沉重的負擔,因此在非密封類中應該避免實現ICloneable
介面,但這個不是ICloneable
特有的缺陷,任何一種方式實現原型模式都存在該問題,因此建議將原型模式的實現類設定為密封類。 - Clone方法返回值是
object
,是非型別安全的;
ICloneable
被很多人認為是一個糟糕的設計,其他理由如下:
ICloneable
除了標識可被克隆之外,無論作為引數還是返回值都沒有任何意義;.Net Framework
在升級支援泛型至今,都沒有新增一個與之對應的ICloneable<T>
泛型介面;- 很多框架中為了向下相容,雖然實現了
ICloneable
介面,但是內部只提供了一個丟擲異常的私有實現,例如SqlConnection
。
鑑於上述諸多缺點,在實現原型模式時,ICloneable
介面能不用就不要用了,自己定義一個更有意義的方法或許會更好。
總結
原型模式通常用在物件建立複雜或者建立過程需要消耗大量資源的場景。但由於其實現過程中會存在諸多問題,如果處理不當很容易對使用者造成困擾,因此,應儘量使用序列化反序列化的方式實現,儘量將其標記為sealed
,另外,儘量避免對ICloneable
介面的使用。