設計模式的征途—5.原型(Prototype)模式

Edison Chou發表於2017-05-06

相信大多數的人都看過《西遊記》,對孫悟空拔毛變出小猴子的故事情節應該都很熟悉。孫悟空可以用猴毛根據自己的形象複製出很多跟自己一模一樣的小猴兵出來,其實在設計模式中也有一個類似的模式,我們可以通過一個原型物件來克隆出多個一模一樣的物件,這個模式就是原型模式。

原型模式(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資源或者網路資源),新的物件可以通過原型模式對已有物件進行復制來獲得。如果是相似物件,則可以對其成員變數稍作修改。

參考資料

      DesignPattern

  劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

 

 

相關文章