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

相信大多數的人都看過《西遊記》,對孫悟空拔毛變出小猴子的故事情節應該都很熟悉。孫悟空能夠用猴毛根據本身的形象複製出不少跟本身如出一轍的小猴兵出來,其實在設計模式中也有一個相似的模式,咱們能夠經過一個原型對象來克隆出多個如出一轍的對象,這個模式就是原型模式。編程

原型模式(Prototype) 學習難度:★★★☆☆ 使用頻率:★★★☆☆

1、大同小異的工做週報

  M公司一直在使用自行開發的一個OA系統進行平常工做辦理,但在使用過程當中,愈來愈多的人對工做週報的建立和編寫模塊產生了抱怨。追其緣由,M公司的OA管理員發現,因爲某些崗位每週工做存在重複性,工做週報內容都大同小異,以下圖所示:設計模式

  這些週報只有一些小地方存在差別,可是現行系統每週默認建立的週報都是空白報表,所以用戶只能經過從新輸入或不斷地複製與粘貼來填寫重複的週報內容,極大地下降了工做效率,浪費寶貴的時間。如何快速建立相同或者類似的工做週報,成爲了M公司軟件開發人員的一個新問題。網絡

  M公司開發人員通過分析,決定按照如下思路對工做週報模塊進行從新設計:ide

  (1)除了容許用戶建立新週報外,還容許用戶將建立好的週報保存爲模板(也就是原型)。學習

  (2)用戶在再次建立週報時,能夠建立全新的週報,還能夠選擇合適的模板複製生成一個相同的週報,而後對新生成的週報根據實際狀況進行修改,產生新的週報。測試

2、原型模式概述

2.1 關於原型模式

  原型模式的原理很簡單,將一個原型對象傳給那個要發動建立的對象,這個要發動建立的對象經過請求原型對象克隆本身來實現建立過程。this

原型模式(Prototype):使用原型實例指定建立對象的種類,而且經過拷貝這些原 型建立新的對象。原型模式是一種對象建立型模式。spa

  須要注意的是,經過克隆方法所建立的對象時全新的對象。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、基於原型模式的工做週報

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、原型模式深刻之原型管理器

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、原型模式總結

5.1 主要優勢

  (1)當建立新的對象實例較爲複雜時,使用原型模式能夠簡化對象的建立過程,經過複製一個已有的實例能夠提升新實例的建立效率。

  (2)可使用深複製的方式保存對象的狀態。將對象複製一份並將其狀態保存起來,以便於在使用的時候使用,好比恢復到某一個歷史狀態,能夠輔助實現撤銷操做。

5.2 主要缺點

  (1)須要爲每個類配備一個克隆方法,並且該克隆方法位於一個類的內部,當對已有的類進行改造時,須要修改源代碼,違背了開閉原則

  (2)爲了支持深複製,當對象之間存在多重嵌套引用關係時,每一層對象都必須支持深複製,實現起來可能比較麻煩。

5.3 應用場景

  最主要的應用場景就在於 建立新對象成本較大(例如初始化須要佔用較長的時間,佔用太多的CPU資源或者網絡資源),新的對象能夠經過原型模式對已有對象進行復制來得到。若是是類似對象,則能夠對其成員變量稍做修改。

參考資料

      DesignPattern

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

 

相關文章
相關標籤/搜索