設計模式-原型模式(Prototype)【重點:淺複製與深複製】

稻草堆上打著滾兒發表於2020-04-24

講故事

最近重溫了一下星爺的《唐伯虎點秋香》,依然讓我捧腹不已,幻想著要是我也能有一名秋香如此的侍女,夫復何求呀,帶著這個美好的幻想沉沉睡去...

突然想到,我是一名程式猿呀,想要什麼物件不是易如反掌嗎,New一個唄,不光是秋香,春、夏、冬都要,身材要超A的,百度好三圍(82, 54, 86),開幹...

Coding

Beauty類,包含美人的屬性

    public class Beauty
    {
        public Beauty(int bust, int theWaist, int hipline)
        {
            Bust = bust;
            TheWaist = theWaist;
            Hipline = hipline;
            //模擬產生一名美人的時長
            Thread.Sleep(3000);
        }

        /// <summary>
        /// 名稱
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 才藝
        /// </summary>
        public string Talent { get; set; }

        /// <summary>
        /// 胸圍
        /// </summary>
        public int Bust { get; set; }

        /// <summary>
        /// 腰圍
        /// </summary>
        public int TheWaist { get; set; }

        /// <summary>
        /// 臀圍
        /// </summary>
        public int Hipline { get; set; }

        /// <summary>
        /// 起名
        /// </summary>
        /// <param name="name"></param>
        public void SetName(string name)
        {
            Name = name;
        }

        /// <summary>
        /// 設定才藝
        /// </summary>
        /// <param name="talent"></param>
        public void SetTalent(string talent)
        {
            Talent = talent;
        }
    }

客戶端生產美女

    internal class Program
    {
        private static void Main(string[] args)
        {
            var sw = new Stopwatch();
            sw.Start();
            var beauty1 = new Beauty(82, 54, 86);
            sw.Stop();
            Console.WriteLine($"生產第一名美人耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty1.SetName("秋香");
            beauty1.SetTalent("彈琴");

            sw.Restart();
            var beauty2 = new Beauty(82, 54, 86);
            sw.Stop();
            Console.WriteLine($"生產第二名美人耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty2.SetName("春香");
            beauty2.SetTalent("畫畫");

            sw.Restart();
            var beauty3 = new Beauty(82, 54, 86);
            sw.Stop();
            Console.WriteLine($"生產第三名美人耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty3.SetName("夏香");
            beauty3.SetTalent("舞蹈");

            Show(beauty1);
            Show(beauty2);
            Show(beauty3);

            Console.WriteLine("\nHappy Ending~");
            Console.ReadLine();
        }

        public static void Show(Beauty beauty)
        {
            Console.WriteLine($"我是 {beauty.Name},身材[{beauty.Bust}-{beauty.TheWaist}-{beauty.Hipline}],我的才藝是 {beauty.Talent}");
        }
    }

結果展示:

生產第一名美女耗時:3008/ms

生產第二名美女耗時:3001/ms

生產第三名美女耗時:3000/ms

我是 秋香,身材[82-54-86],我的才藝是 彈琴
我是 春香,身材[82-54-86],我的才藝是 畫畫
我是 夏香,身材[82-54-86],我的才藝是 舞蹈

我的美人產生了,但就是每次都是通過New建立,設定的標準身材(82-54-86)不變但每次都要設定,而且每次耗時都很長,要是再生產更多,豈不勞累又耗時...

正在苦惱之時,突然靈感乍現,可以使用原型模式(Prototype)解決呀

Code Upgrade

原型模式 用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件

其實就是從一個物件再建立另一個可定製的物件,而且不需要知道任何建立的細節

.NETSystem名稱空間中提供了ICloneable介面,其中只有一個方法Clone(),實現這個介面就可以完成原型模式了。

開始改造:

Beauty實現ICloneable介面

    public class Beauty : ICloneable
    {
        public Beauty(int bust, int theWaist, int hipline)
        {
            Bust = bust;
            TheWaist = theWaist;
            Hipline = hipline;
            //模擬產生一名美人的時長
            Thread.Sleep(3000);
        }

        /// <summary>
        /// 名稱
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 才藝
        /// </summary>
        public string Talent { get; set; }

        /// <summary>
        /// 胸圍
        /// </summary>
        public int Bust { get; set; }

        /// <summary>
        /// 腰圍
        /// </summary>
        public int TheWaist { get; set; }

        /// <summary>
        /// 臀圍
        /// </summary>
        public int Hipline { get; set; }

        /// <summary>
        /// 起名
        /// </summary>
        /// <param name="name"></param>
        public void SetName(string name)
        {
            Name = name;
        }

        /// <summary>
        /// 設定才藝
        /// </summary>
        /// <param name="talent"></param>
        public void SetTalent(string talent)
        {
            Talent = talent;
        }

        public object Clone()
        {
            return this.MemberwiseClone();
        }
    }

客戶端生產時,除第一個呼叫new()外,其他使用Clone()方法建立

    internal class Program
    {
        private static void Main(string[] args)
        {
            var sw = new Stopwatch();
            sw.Start();
            var beauty1 = new Beauty(82, 54, 86);
            sw.Stop();
            Console.WriteLine($"生產第一名美女耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty1.SetName("秋香");
            beauty1.SetTalent("彈琴");

            sw.Restart();
            var beauty2 = (Beauty)beauty1.Clone();
            sw.Stop();
            Console.WriteLine($"生產第二名美女耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty2.SetName("春香");
            beauty2.SetTalent("畫畫");

            sw.Restart();
            var beauty3 = (Beauty)beauty1.Clone();
            sw.Stop();
            Console.WriteLine($"生產第三名美女耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty3.SetName("夏香");
            beauty3.SetTalent("舞蹈");

            Show(beauty1);
            Show(beauty2);
            Show(beauty3);

            Console.WriteLine("\nHappy Ending~");
            Console.ReadLine();
        }

        public static void Show(Beauty beauty)
        {
            Console.WriteLine($"我是 {beauty.Name},身材[{beauty.Bust}-{beauty.TheWaist}-{beauty.Hipline}],我的才藝是 {beauty.Talent}");
        }
    }

結果展示:

生產第一名美女耗時:3009/ms

生產第二名美女耗時:0/ms

生產第三名美女耗時:0/ms

我是 秋香,身材[82-54-86],我的才藝是 彈琴
我是 春香,身材[82-54-86],我的才藝是 畫畫
我是 夏香,身材[82-54-86],我的才藝是 舞蹈

我們可以看到,除了第一個建立耗時以外,其他Clone出來的物件基本不耗時,而且不用重複設定固定屬性(三圍)(一般在初始化的資訊不發生變化的情況下,克隆是最好的方法,既隱藏了物件建立的細節,又對效能大大的提高),我心甚歡...

於是想著對美人的才藝進行升級

Talent類,對才藝描述,增加才藝段位

    public class Talent
    {
        /// <summary>
        /// 才藝名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 段位
        /// </summary>
        public int Level { get; set; }
    

Beauty類改造

    public class Beauty : ICloneable
    {
        public Beauty(int bust, int theWaist, int hipline)
        {
            Bust = bust;
            TheWaist = theWaist;
            Hipline = hipline;
            Talent = new Talent();
            //模擬產生一名美人的時長
            Thread.Sleep(3000);
        }

        /// <summary>
        /// 名稱
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 才藝
        /// </summary>
        public Talent Talent { get; set; }

        /// <summary>
        /// 胸圍
        /// </summary>
        public int Bust { get; set; }

        /// <summary>
        /// 腰圍
        /// </summary>
        public int TheWaist { get; set; }

        /// <summary>
        /// 臀圍
        /// </summary>
        public int Hipline { get; set; }

        /// <summary>
        /// 起名
        /// </summary>
        /// <param name="name"></param>
        public void SetName(string name)
        {
            Name = name;
        }

        /// <summary>
        /// 設定才藝
        /// </summary>
        /// <param name="talent"></param>
        public void SetTalent(string name, int level)
        {
            Talent.Name = name;
            Talent.Level = level;
        }

        public object Clone()
        {
            return this.MemberwiseClone();
        }
    }

客戶端設定才藝段位:

    internal class Program
    {
        private static void Main(string[] args)
        {
            var sw = new Stopwatch();
            sw.Start();
            var beauty1 = new Beauty(82, 54, 86);
            sw.Stop();
            Console.WriteLine($"生產第一名美女耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty1.SetName("秋香");
            beauty1.SetTalent("彈琴", 10);

            sw.Restart();
            var beauty2 = (Beauty)beauty1.Clone();
            sw.Stop();
            Console.WriteLine($"生產第二名美女耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty2.SetName("春香");
            beauty2.SetTalent("畫畫", 9);

            sw.Restart();
            var beauty3 = (Beauty)beauty1.Clone();
            sw.Stop();
            Console.WriteLine($"生產第三名美女耗時:{sw.ElapsedMilliseconds}/ms\n");
            beauty3.SetName("夏香");
            beauty3.SetTalent("舞蹈", 8);

            Show(beauty1);
            Show(beauty2);
            Show(beauty3);

            Console.WriteLine("\nHappy Ending~");
            Console.ReadLine();
        }

        public static void Show(Beauty beauty)
        {
            Console.WriteLine($"我是 {beauty.Name},身材[{beauty.Bust}-{beauty.TheWaist}-{beauty.Hipline}],我的才藝是 {beauty.Talent.Name} 段位 {beauty.Talent.Level}");
        }
    }

結果展示:

生產第一名美女耗時:3022/ms

生產第二名美女耗時:0/ms

生產第三名美女耗時:0/ms

我是 秋香,身材[82-54-86],我的才藝是 舞蹈 段位 8
我是 春香,身材[82-54-86],我的才藝是 舞蹈 段位 8
我是 夏香,身材[82-54-86],我的才藝是 舞蹈 段位 8

看到結果我懵了,什麼情況,我明明把才藝設定的不一樣,怎麼三個最後都是 [舞蹈 段位 8] ???

良久平息之後,才明白導致上面結果的原因,也將是本文的重點:淺複製與深複製

淺複製與深複製

淺複製:複製得到的物件的所有變數都包含有與原來的物件相同的值,而所有的對其他物件的引用都仍然指向原來的物件。

MemberwiseClone()方法就是淺複製,如果欄位是值型別的,則對該欄位執行逐位複製,如果欄位是引用型別,則複製引用但不復制引用物件,因此原始物件及其複本引用同一物件。

這就是上面,三個美人的才藝最後都變成一樣的原因,才藝Talent是一個引用型別,複製三份的是Talent的引用,都指向同一個Talent物件

深複製:把引用物件的變數指向複製過的新物件,而不是原有的被引用的物件。

解決上面的問題,我們就要用深複製,要對Talent複製一個新物件,而不是一個引用。('美人'引用了'才藝',假如'才藝'裡還有 引用,很多層,就涉及到深複製要深入多少層的問題,需要事先考慮好,而且要當心出現迴圈引用的問題,本次案例我們就深入到第一層就可以了)

上程式碼:

Talent類,實現ICloneable介面

    public class Talent : ICloneable
    {
        /// <summary>
        /// 才藝名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 段位
        /// </summary>
        public int Level { get; set; }

        public object Clone()
        {
            return this.MemberwiseClone();
        }
    }

Beauty類,增加私有建構函式,以便克隆“才藝”的克隆資料

    public class Beauty : ICloneable
    {
        public Beauty(int bust, int theWaist, int hipline)
        {
            Bust = bust;
            TheWaist = theWaist;
            Hipline = hipline;
            Talent = new Talent();
            //模擬產生一名美人的時長
            Thread.Sleep(3000);
        }

        private Beauty(Talent talent)
        {
            this.Talent = (Talent)talent.Clone();
        }

        /// <summary>
        /// 名稱
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 才藝
        /// </summary>
        public Talent Talent { get; set; }

        /// <summary>
        /// 胸圍
        /// </summary>
        public int Bust { get; set; }

        /// <summary>
        /// 腰圍
        /// </summary>
        public int TheWaist { get; set; }

        /// <summary>
        /// 臀圍
        /// </summary>
        public int Hipline { get; set; }

        /// <summary>
        /// 起名
        /// </summary>
        /// <param name="name"></param>
        public void SetName(string name)
        {
            Name = name;
        }

        /// <summary>
        /// 設定才藝
        /// </summary>
        /// <param name="talent"></param>
        public void SetTalent(string name, int level)
        {
            Talent.Name = name;
            Talent.Level = level;
        }

        public object Clone()
        {
            //呼叫私有構造方法,讓“才藝”克隆完成,然後再給這個 “美人” 物件的相關欄位賦值,
            //最終返回一個深複製的 “美人” 物件
            var beauty = new Beauty(Talent)
            {
                Bust = this.Bust,
                TheWaist = this.TheWaist,
                Hipline = this.Hipline
            };
            return beauty;
        }
    }

客戶端通以前一樣,展示結果:

生產第一名美女耗時:3008/ms

生產第二名美女耗時:0/ms

生產第三名美女耗時:0/ms

我是 秋香,身材[82-54-86],我的才藝是 彈琴 段位 10
我是 春香,身材[82-54-86],我的才藝是 畫畫 段位 9
我是 夏香,身材[82-54-86],我的才藝是 舞蹈 段位 8

看到結果,我心甚喜。突然屋內彩虹環繞,三位美人出現在我面前,婀娜妖嬈,不對,怎麼少了我的冬香,爬向鍵盤...

叮鈴鈴,叮鈴鈴...鬧鈴響了

握艹,我要續夢~

原始碼地址: https://gitee.com/sayook/DesignMode/tree/master/Prototype

相關文章