設計模式的征途—5.原型(Prototype)模式
相信大多數的人都看過《西遊記》,對孫悟空拔毛變出小猴子的故事情節應該都很熟悉。孫悟空可以用猴毛根據自己的形象復制出很多跟自己一模一樣的小猴兵出來,其實在設計模式中也有一個類似的模式,我們可以通過一個原型對象來克隆出多個一模一樣的對象,這個模式就是原型模式。
原型模式(Abstract Factory) | 學習難度:★★★☆☆ | 使用頻率:★★★☆☆ |
一、大同小異的工作周報
M公司一直在使用自行開發的一個OA系統進行日常工作辦理,但在使用過程中,越來越多的人對工作周報的創建和編寫模塊產生了抱怨。追其原因,M公司的OA管理員發現,由於某些崗位每周工作存在重復性,工作周報內容都大同小異,如下圖所示:
這些周報只有一些小地方存在差異,但是現行系統每周默認創建的周報都是空白報表,因此用戶只能通過重新輸入或不斷地復制與粘貼來填寫重復的周報內容,極大地降低了工作效率,浪費寶貴的時間。如何快速創建相同或者相似的工作周報,成為了M公司軟件開發人員的一個新問題。
M公司開發人員經過分析,決定按照以下思路對工作周報模塊進行重新設計:
(1)除了允許用戶創建新周報外,還允許用戶將創建好的周報保存為模板(也就是原型)。
(2)用戶在再次創建周報時,可以創建全新的周報,還可以選擇合適的模板復制生成一個相同的周報,然後對新生成的周報根據實際情況進行修改,產生新的周報。
二、原型模式概述
2.1 關於原型模式
原型模式的原理很簡單,將一個原型對象傳給那個要發動創建的對象,這個要發動創建的對象通過請求原型對象克隆自己來實現創建過程。
原型模式(Prototype):使用原型實例指定創建對象的種類,並且通過拷貝這些原 型創建新的對象。原型模式是一種對象創建型模式。
需要註意的是,通過克隆方法所創建的對象時全新的對象。
原型模式的結構如下圖所示:
● Prototype(抽象原型類):它是聲明克隆方法的接口,是所有具體原型類的公共父類,可以是抽象類也可以是接口,甚至還可以是具體實現類。
● ConcretePrototype(具體原型類):它實現在抽象原型類中聲明的克隆方法,在克隆方法中返回自己的一個克隆對象
● Client(客戶類):讓一個原型對象克隆自身從而創建一個新的對象,在客戶類中只需要直接實例化或通過工廠方法等方式創建一個原型對象,再通過調用該對象的克隆方法即可得到多個相同的對象。由於客戶類針對抽象原型類Prototype編程,因此用戶可以根據需要選擇具體原型類,系統具有較好的可擴展性,增加或更換具體原型類都很方便。
2.2 基本實現方法
(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; } }
三、基於原型模式的工作周報
3.1 設計思路
M公司開發人員決定使用原型模式來實現工作周報的快速創建:
這裏,Object相當於抽象原型類,而所有實現了ICloneable接口的類都相當於具體原型類。
3.2 實現代碼
(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); } }
執行結果如下圖所示:
3.3 帶附件的周報
經過改進後的工作周報已經獲得用戶的一致好評,但是,又有員工提出有些周報帶有附件,如果使用上面的實現,周報的附件並不能夠復制成功。在進入設計之前,我們先來了解一下淺復制和深復制。
(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])); }
此時,借助深復制克隆的對象已經不再是指向同一個內存地址的了,因此兩個附件也是不同的:
四、原型模式深入之原型管理器
4.1 何為原型管理器
原型管理器(Prototype Manager)將多個原型對象存儲在一個集合中供客戶端使用,它是一個專門負責克隆對象的工廠,其中定義了一個集合用於存儲原型對象,如果需要某個原型對象的一個克隆,可以通過復制集合中對應的原型對象來獲得。在原型管理器中針對抽象原型類進行編程,以便於擴展。
原型管理器對應的結構圖如下:
4.2 公文管理器的設計與實現
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)); }
運行結果如下:
五、原型模式總結
5.1 主要優點
(1)當創建新的對象實例較為復雜時,使用原型模式可以簡化對象的創建過程,通過復制一個已有的實例可以提高新實例的創建效率。
(2)可以使用深復制的方式保存對象的狀態。將對象復制一份並將其狀態保存起來,以便於在使用的時候使用,比如恢復到某一個歷史狀態,可以輔助實現撤銷操作。
5.2 主要缺點
(1)需要為每一個類配備一個克隆方法,而且該克隆方法位於一個類的內部,當對已有的類進行改造時,需要修改源代碼,違背了開閉原則。
(2)為了支持深復制,當對象之間存在多重嵌套引用關系時,每一層對象都必須支持深復制,實現起來可能比較麻煩。
5.3 應用場景
最主要的應用場景就在於 創建新對象成本較大(例如初始化需要占用較長的時間,占用太多的CPU資源或者網絡資源),新的對象可以通過原型模式對已有對象進行復制來獲得。如果是相似對象,則可以對其成員變量稍作修改。
參考資料
劉偉,《設計模式的藝術—軟件開發人員內功修煉之道》
作者:周旭龍
出處:http://edisonchou.cnblogs.com
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
設計模式的征途—5.原型(Prototype)模式