[設計模式-7] 原型模式

話三國~原型模式

發矯詔諸鎮應曹公

曹操自從刺殺董卓失敗之後,逃出洛陽,為了天下蒼生,便下定決心要假借天子詔的名義廣發檄文爭討董卓.曹操清點了一下目前有勢力可以加入討董行列的諸侯,如下列名單所示:第一鎮,後將軍南陽太守袁術。第二鎮,冀州刺史韓馥。第三鎮,豫州刺史孔鈾。第四鎮,兗州刺史劉岱。第五鎮,河內郡太 守王匡。第六鎮,陳留太守張邈。第七鎮,東郡太守喬瑁。第八鎮,山陽太守袁遺 。第九鎮,濟北相鮑信。第十鎮,北海太守孔融。第十一鎮,廣陵太守張超。第十二鎮,徐州刺史陶謙。第十三鎮,西涼太守馬騰。第十四鎮,北平太守公孫瓚。第 十五鎮,上黨太守張楊。第十六鎮,烏程侯長沙太守孫堅。第十七鎮,祁鄉侯渤海太守袁紹加上自己共十八鎮諸侯.

  檄文的格式如下 

       袁紹聽令(Owner)
       奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓  (Context)
       漢獻帝印 欽此 (Footer)
       

        以上故事,就直接用最簡單的方式來寫看看吧.

    /// <summary>
    /// 征討檄文
    /// </summary>  
   class ConveneDocument
     {
        private string _owner;//發送對象
        private string _context;//內文 
        private string _footer;//註腳
        
        public ConveneDocument(string context, string footer)
        {
            _context = context;
            _footer = footer;
        }

        public void SetOwner(string name) 
        {
            _owner = name + "聽令!! ";
            StringBuilder command = new StringBuilder();
            command.Append(_owner);
            command.Append(_context);
            command.Append(_footer);
            Console.WriteLine(command.ToString());  
        }
    }

首先,定義ConveneDocument(檄文)的類別,在建構子裡接收兩個參數,來設定Context和Footer;並且提供一個SetOwner方法來指定並發送給特定對象.

        static void Main(string[] args)
        {
            var 檄文1 = new ConveneDocument("奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 ", "漢獻帝印 欽此");
            檄文1.SetOwner("孫堅");

            var 檄文2 = new ConveneDocument("奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 ", "漢獻帝印 欽此");
            檄文2.SetOwner("袁術");

            var 檄文3 = new ConveneDocument("奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 ", "漢獻帝印 欽此");
            檄文3.SetOwner("公孫讚");

            var 檄文4 = new ConveneDocument("奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 ", "漢獻帝印 欽此");
            檄文4.SetOwner("袁紹");

            Console.Read();
        }

接著來寫主程式,曹操在這裡只發了4封檄文,就覺得有點寫不下去了,因為密密麻麻的重複程式碼,等到把18路諸侯的檄文都寫齊,恐怕已經是一個災難.如果檄文的內容有要做修正,那一改就是18個地方要改,這實在是一件非常蠢的事.此時,曹操心想,假如能把檄文內容直接複製一份,然後把發送對象改掉就好,那一切將會變得容易許多.而在設計模式裡,原型模式就是在專處理這種情境.

定義

原型模式=>透過拷貝的方式來建立新物件,而不需要再對新物件基於原型基礎拷貝來的屬性重新做設定。

    /// <summary>
    /// 征討檄文
    /// </summary>
    class ConveneDocument : ICloneable//要實作ICloneable介面
    {
        private string _context;
        private string _footer;
        private string _owner;

        public ConveneDocument(string context, string footer)
        {
            _context = context;
            _footer = footer;
        }

        //ICloneable介面定義的方法
        public object Clone()
        {
            return this.MemberwiseClone();
        }

        public void SetOwner(string name) 
        {
            _owner = name + "聽令!! ";
            StringBuilder command = new StringBuilder();
            command.Append(_owner);
            command.Append(_context);
            command.Append(_footer);
            Console.WriteLine(command.ToString());  
        }
    }

實作的方式非常簡單,只需要將原本的類別標記成有實作ICloneable介面,接著實作Clone方法,而實作的方式直接呼叫MemberwiseClone()方法即可.

    class Program
    {
        static void Main(string[] args)
        {
            var 檄文1 = new ConveneDocument("奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 ", "漢獻帝印 欽此");
            檄文1.SetOwner("孫堅");

            var 檄文2 = 檄文1.Clone() as ConveneDocument;
            檄文2.SetOwner("袁術");

            var 檄文3 = 檄文1.Clone() as ConveneDocument;
            檄文3.SetOwner("公孫讚");

            var 檄文4 = 檄文1.Clone() as ConveneDocument;
            檄文4.SetOwner("袁紹");

            Console.Read();
        }
    }

主程式透過呼叫Clone的方法,便可拷貝Context和Footer而不需要再重新做設定,程式碼簡潔了不少,並且也達到了預期的功能.附帶一提,原型模式還有一個很適用的情境,便是假如某物件的建構需要很長的時間,透過簡單的Clone方式可以迅速地建立新物件,並且保有基於原型模式的所有設定.

就當一切運作的都非常順利的時候,此時出現了一個新需求,除了發送檄文告知對方前來之外,曹操希望能夠同時告知對方需要帶領多少軍力和將軍前來.

    /// <summary>
    /// 軍隊數量
    /// </summary>
    class ArmyAmount
    {
        public int soilder = 0;//兵力數量
        public int general = 0;//將軍數量
        public ArmyAmount(int soilder,int general)
        {
            this.soilder = soilder;
            this.general = general;
        }
    }

為了達到此一需求,我們宣告了一個ArmyAmount類別,來存放需要率領多少將軍和士兵的資訊.

    /// <summary>
    /// 征討檄文
    /// </summary>
    class ConveneDocument : ICloneable
    {
        private string _context;
        private string _footer;
        private string _owner;
        public ArmyAmount army=new ArmyAmount(0,0);

        public ConveneDocument(string context, string footer)
        {
            _context = context;
            _footer = footer;
        }

        public object Clone()
        {
            var result = this.MemberwiseClone() as ConveneDocument;
            return result;
        }

        //新增設定軍隊規模的方法
        public void SetArmy(int  general,int soilder)
        {
            army.general = general ;
            army.soilder = soilder;
        }

        public void SetOwner(string name) {
            _owner = name + "聽令!! ";
            Console.WriteLine(_owner);
            Console.WriteLine(_context);
            
            //新增軍隊規模的輸出
            Console.WriteLine("令率戰將" + army.general + " 大軍" + army.soilder);
            
            Console.WriteLine(_footer);
            Console.WriteLine();
        }
    }

並且在ConveneDocument類別新增SetArmy的方法用來設定ArmyAmount(軍隊數量),並且在SetOwner的地方額外輸出共率領多少軍隊的字樣.

 class Program
    {
        static void Main(string[] args)
        {
            var 檄文1 = new ConveneDocument("奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 ", "漢獻帝印 欽此");
            檄文1.SetArmy(10,30000);
            檄文1.SetOwner("孫堅");
            Console.WriteLine();

            var 檄文2 = 檄文1.Clone() as ConveneDocument;
            檄文2.SetArmy(5, 20000);
            檄文2.SetOwner("袁術");
            Console.WriteLine();

            var 檄文3 = 檄文1.Clone() as ConveneDocument;
            檄文3.SetArmy(2, 10000);
            檄文3.SetOwner("公孫讚");
            Console.WriteLine();

            var 檄文4 = 檄文1.Clone() as ConveneDocument;
            檄文4.SetArmy(20, 60000);
            檄文4.SetOwner("袁紹");
            Console.WriteLine();

            Console.Read();
        }
    }

主程式同樣透過呼叫Clone的方法,拷貝Context和Footer而不需要再重新做設定,之後呼叫SetArmy(設定軍隊規模)和SetOwner(發送檄文給指定對象).我們來看看輸出.

程式碼輸出:
孫堅聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將20 大軍60000
漢獻帝印 欽此

袁術聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將20 大軍60000
漢獻帝印 欽此

公孫瓚聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將20 大軍60000
漢獻帝印 欽此

袁紹聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將20 大軍60000
漢獻帝印 欽此

所有諸侯都叫苦聊天,因18鎮諸侯,屬袁紹勢力最大,也是唯一有能力動員大軍60000,戰將20人的諸侯,可問題是曹操在設計檄文的時候,本來就有根據各諸侯可動員的兵力來做檄文的撰寫,怎會到後來全部檄文的軍隊規模都變成袁紹的版本呢?其實,這問題並不難理解,就是複製value和複製reference的問題.如果是使用MemberwiseClone,針對物件只會拷貝reference,也就造成其實所有諸侯的army屬性都是指向同一個ArmyAmount的物件,而好死不死,袁紹又是最後一個做army設定的,導致所有諸侯的軍隊規模版本都變成袁紹的版本.那要如何實作出連物件的拷貝都是拷貝成新的物件實體呢?接著就來看看下面的程式碼.

    /// <summary>
    /// 軍隊數量
    /// </summary>
    class ArmyAmount:ICloneable//也要宣告實作ICloneable介面
    {
        public int general = 0;
        public int soilder = 0;
        public ArmyAmount(int general,int soilder)
        {
            this.general = general;
            this.soilder = soilder;
        }

        //實作方式跟前面一樣
        public object Clone()
        {
            return this.MemberwiseClone();
        }
    }

針對ArmyAmount(軍隊數量)的物件必須也要實作ICloneable介面,並且透過呼叫MemberwiseClone來實作Clone方法.

    /// <summary>
    /// 征討檄文
    /// </summary>
    class ConveneDocument : ICloneable
    {
        private string _context;
        private string _footer;
        private string _owner;
        public ArmyAmount army=new ArmyAmount(0,0);

        public ConveneDocument(string context, string footer)
        {
            _context = context;
            _footer = footer;
        }

        public object Clone()
        {
            var result = this.MemberwiseClone() as ConveneDocument;

            //army的拷貝也是採用拷貝新的實體而非只有參考
            result.army = result.army.Clone() as ArmyAmount;
            
            return result;
        }

        public void SetArmy(int  general,int soilder)
        {
            army.general = general ;
            army.soilder = soilder;
        }

        public void SetOwner(string name) {
            _owner = name + "聽令!! ";
            Console.WriteLine(_owner);
            Console.WriteLine(_context);
            
            //新增軍隊規模的輸出
            Console.WriteLine("令率戰將" + army.general + " 大軍" + army.soilder);
            
            Console.WriteLine(_footer);
            Console.WriteLine();
        }
    }

原本的檄文類別只需要做小修改,Clone的方法額外新增針對Army屬性是呼叫ArmyAmount類別所提供的Clone方法,如此便能達成不是拷貝物件參考,而是完完全全拷貝出一個新實體的需求.接著便可以直接來看程式碼的輸出.

程式碼輸出:
孫堅聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將10 大軍30000
漢獻帝印 欽此

袁術聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將5 大軍20000
漢獻帝印 欽此

公孫瓚聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將2 大軍10000
漢獻帝印 欽此

袁紹聽令
奉天承運,皇帝詔曰:令爾等即刻出兵征伐董卓 
令率戰將20 大軍60000
漢獻帝印 欽此

完成,曹操看到檄文的內容非常滿意,而各諸侯也真的如期前來赴約,看來剿滅董卓的日子指日可待了,以上便是原型模式的簡單分享....