
相信大多數的人都看過《西遊記》,對孫悟空拔毛變出小猴子的故事情節應該都很熟悉。孫悟空可以用猴毛根據自己的形象複制出很多跟自己一模一樣的小猴兵出來,其實在設計模式中也有一個類似的模式,我們可以通過一個原型對象來克隆出多個一模一樣的對象,這個模式就是原型模式。原型模式的原理很簡單,将一個原型對象傳給那個要發動建立的對象,這個要發動建立的對象通過請求原型對象克隆自己來實作建立過程。
相信大多數的人都看過《西遊記》,對孫悟空拔毛變出小猴子的故事情節應該都很熟悉。孫悟空可以用猴毛根據自己的形象複制出很多跟自己一模一樣的小猴兵出來,其實在設計模式中也有一個類似的模式,我們可以通過一個原型對象來克隆出多個一模一樣的對象,這個模式就是原型模式。
原型模式(Prototype) | 學習難度:★★★☆☆ | 使用頻率:★★★☆☆ |
一、大同小異的工作周報
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
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。