C#設計模式-原型模式(Prototype Pattern)

Tynam.Yang發表於2020-11-20

引言

在軟體開發過程中,我們習慣使用new來建立物件。但是當我們建立一個例項的過程很昂貴或者很複雜,並且需要建立多個這樣的類的例項時。如果仍然用new操作符去建立這樣的類的例項,會導致記憶體中多分配一個一樣的類例項物件,增加建立類的複雜度和消耗更多的記憶體空間。
如果採用簡單工廠模式來建立這樣的系統。隨著產品類增加,子類數量不斷增加,會增加額外系統複雜程度,為此我們不得不引入原型模式了。

概念

原型模式(Prototype Pattern)是一種建立型設計模式, 使你能夠複製物件, 甚至是複雜物件, 而又無需使程式碼依賴它們所屬的類。
通過複製一個已經存在的例項來建立一個新的例項,而且不需知道任何建立的細節。被複制的例項被稱為原型,這個原型是可定製的。
所有的原型類都必須有一個通用的介面, 使得即使在物件所屬的具體類未知的情況下也能複製物件。 原型物件可以生成自身的完整副本, 因為相同類的物件可以相互訪問對方的私有成員變數。

結構圖

原型模式下主要角色:

  • 原型(Prototype):宣告一個克隆自身的介面,該角色一般有抽象類(Prototype)、介面(ICloneable)兩種實現方式。
  • 具體原型類(ConcretePrototype):實現原型(抽象類或介面)的 Clone() 方法,它是可被複制的物件。
  • 訪問類(Client):使用具體原型類中的 Clone() 方法來複制新的物件。

實現

假如有一個測試用例模板,專案A正在使用,公司又引進一個專案B,專案B的測試用例模板自己重新寫一套肯定非常麻煩,那麼可以使用專案A的用例模板,拿來改改就可以使用了。省卻了許多時間。

使用淺拷貝實現

淺拷貝:將原來物件中的所有欄位逐個複製到一個新物件,如果欄位是值型別,則簡單地複製一個副本到新物件,改變新物件的值型別欄位不會影響原物件;如果欄位是引用型別,則複製的是引用,改變目標物件中引用型別欄位的值將會影響原物件。例如, 如果一個物件有一個指向引用型別(如測試用例的名稱)的欄位, 並且我們對該物件做了一個淺複製, 那麼兩個物件將引用同一個引用(即同一個測試用例名稱)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Prototype
{
    class Program
    {
        static void Main(string[] args)
        {
            TestCase projectALoginCase = new TestCase
            {
                Id = 1001,
                ProjectName = "A專案",
                CreatTime = new DateTime(2020, 11, 19),
            };
            projectALoginCase.SetTestCaseContent("登入測試", "", "開啟登入頁面並且登入", "登入成功");


            TestCase projectBLoginCase = (TestCase)projectALoginCase.Clone();
            projectBLoginCase.ProjectName = "B專案";

            projectALoginCase.Show();
            projectBLoginCase.Show();
            Console.Read();
        }
    }

    /// <summary>
    /// 實現了 ICloneable 介面
    /// </summary>
    public class TestCase : ICloneable
    {
        public TestCase()
        {
            mTestCaseContent = new TestCaseContent();
        }

        private int id;
        private string projectName;
        private DateTime creatTime;
        private TestCaseContent mTestCaseContent;

        public int Id
        {
            get { return id; }
            set { id = value; }
        }

        public string ProjectName
        {
            get { return projectName; }
            set { projectName = value; }
        }

        public DateTime CreatTime
        {
            get { return creatTime; }
            set { creatTime = value; }
        }

        
        public void Show()
        {
            Console.WriteLine($"Id:\t{this.Id}");
            Console.WriteLine($"ProjectName:\t{this.ProjectName}");
            Console.WriteLine($"CreatTime:\t{this.CreatTime}");
            if (this.TestCaseContent != null)
            {
                this.TestCaseContent.show();
            }
            Console.WriteLine("=================================================");

        }

        /// <summary>
        /// 關聯一個引用型別
        /// </summary>
        public TestCaseContent TestCaseContent
        {
            get { return mTestCaseContent; }
        }
        public void SetTestCaseContent(string Name, string Level, string Step, string ExpectedResults)
        {
            this.mTestCaseContent.Name = Name;
            this.mTestCaseContent.Level = Level;
            this.mTestCaseContent.Step = Step;
            this.mTestCaseContent.ExpectedResults = ExpectedResults;
        }

        public object Clone()
        {
            // 淺拷貝物件的方法
            return this.MemberwiseClone();
        }
    }

    /// <summary>
    /// 測試用例內容類
    /// </summary>
    public class TestCaseContent
    {
        public string Name { get; set; }
        public string Level { get; set; }
        public string Step { get; set; }
        public string ExpectedResults { get; set; }

        public void show() {
            Console.WriteLine($"Name:\t{this.Name}");
            Console.WriteLine($"Level:\t{this.Level}");
            Console.WriteLine($"Step:\t{this.Step}");
            Console.WriteLine($"ExpectedResults:\t{this.ExpectedResults}");
        }
    }
}

執行後結果

Id:    1001
ProjectName:    A專案
CreatTime:    11/19/2020 12:00:00 AM
Name:    登入測試
Level:    高
Step:    開啟登入頁面並且登入
ExpectedResults:    登入成功
=================================================
Id:    1001
ProjectName:    B專案
CreatTime:    11/19/2020 12:00:00 AM
Name:    登入測試
Level:    高
Step:    開啟登入頁面並且登入
ExpectedResults:    登入成功
=================================================

如果我們將拷貝後的專案B的測試用例的值進行重新設定,如下程式碼:

static void Main(string[] args)
        {
            TestCase projectALoginCase = new TestCase
            {
                Id = 1001,
                ProjectName = "A專案",
                CreatTime = new DateTime(2020, 11, 19),
            };
            projectALoginCase.SetTestCaseContent("登入測試", "", "開啟登入頁面並且登入", "登入成功");


            TestCase projectBLoginCase = (TestCase)projectALoginCase.Clone();
            projectBLoginCase.ProjectName = "B專案";
            projectBLoginCase.SetTestCaseContent("B專案登入測試", "級別高", "開啟登入頁面並且登入", "登入成功");

            projectALoginCase.Show();
            projectBLoginCase.Show();
            Console.Read();
        }

再次執行結果如下:

Id:    1001
ProjectName:    A專案
CreatTime:    11/19/2020 12:00:00 AM
Name:    B專案登入測試
Level:    級別高
Step:    開啟登入頁面並且登入
ExpectedResults:    登入成功
=================================================
Id:    1001
ProjectName:    B專案
CreatTime:    11/19/2020 12:00:00 AM
Name:    B專案登入測試
Level:    級別高
Step:    開啟登入頁面並且登入
ExpectedResults:    登入成功
=================================================

可以看的,通過淺拷貝後實際複製的是引用,改變目標物件中引用型別欄位的值將會影響原物件。對於上面的例項顯然是不可取的。修改B專案的測試用例影響到了A專案,肯定是有問題的。
接下來介紹使用深拷貝進行實現。

使用深拷貝實現

深拷貝:與淺複製不同之處在於對引用型別的處理,深複製將新物件中引用型別欄位指向複製過的新物件,改變新物件中引用的任何物件,不會影響到原來的物件中對應欄位的內容。例如,如果一個物件有一個指向引用型別(如測試用例的名稱)的欄位,並且對該物件做了一個深複製的話,將建立一個新的物件(即新的測試用例名稱)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Prototype
{
    class Program
    {
        static void Main(string[] args)
        {
            TestCase projectALoginCase = new TestCase
            {
                Id = 1001,
                ProjectName = "A專案",
                CreatTime = new DateTime(2020, 11, 19),
            };
            projectALoginCase.SetTestCaseContent("登入測試", "", "開啟登入頁面並且登入", "登入成功");


            TestCase projectBLoginCase = (TestCase)projectALoginCase.Clone();
            projectBLoginCase.ProjectName = "B專案";
            projectBLoginCase.SetTestCaseContent("B專案登入測試", "級別高", "開啟登入頁面並且登入", "登入成功");

            projectALoginCase.Show();
            projectBLoginCase.Show();
            Console.Read();
        }
    }

    /// <summary>
    /// 實現了 ICloneable 介面
    /// </summary>
    public class TestCase : ICloneable
    {
        public TestCase()
        {
            mTestCaseContent = new TestCaseContent();
        }

        /// <summary>
        /// 使用私有建構函式對引用型別進行復制
        /// </summary>
        /// <param name="testCaseContent"></param>
        private TestCase(TestCaseContent testCaseContent)
        {
            this.mTestCaseContent = (TestCaseContent)testCaseContent.Clone();
        }

        private int id;
        private string projectName;
        private DateTime creatTime;
        private TestCaseContent mTestCaseContent;

        public int Id
        {
            get { return id; }
            set { id = value; }
        }

        public string ProjectName
        {
            get { return projectName; }
            set { projectName = value; }
        }

        public DateTime CreatTime
        {
            get { return creatTime; }
            set { creatTime = value; }
        }

        
        public void Show()
        {
            Console.WriteLine($"Id:\t{this.Id}");
            Console.WriteLine($"ProjectName:\t{this.ProjectName}");
            Console.WriteLine($"CreatTime:\t{this.CreatTime}");
            if (this.mTestCaseContent != null)
            {
                this.mTestCaseContent.show();
            }
            Console.WriteLine("=================================================");

        }

        /// <summary>
        /// 設定測試用例詳細內容
        /// </summary>
        /// <param name="Name"></param>
        /// <param name="Level"></param>
        /// <param name="Step"></param>
        /// <param name="ExpectedResults"></param>
        public void SetTestCaseContent(string Name, string Level, string Step, string ExpectedResults)
        {
            this.mTestCaseContent.Name = Name;
            this.mTestCaseContent.Level = Level;
            this.mTestCaseContent.Step = Step;
            this.mTestCaseContent.ExpectedResults = ExpectedResults;
        }

        public object Clone()
        {
            // 建立一個全新的測試用例內容
            TestCase newTestCase = new TestCase(this.mTestCaseContent);

            newTestCase.Id = this.Id;
            newTestCase.ProjectName = this.ProjectName;
            newTestCase.CreatTime = this.CreatTime;

            return newTestCase;
        }
    }

    /// <summary>
    /// 測試用例內容類
    /// </summary>
    public class TestCaseContent:ICloneable
    {
        public string Name { get; set; }
        public string Level { get; set; }
        public string Step { get; set; }
        public string ExpectedResults { get; set; }

        public object Clone()
        {
            // 淺拷貝
            return this.MemberwiseClone();
        }

        public void show() {
            Console.WriteLine($"Name:\t{this.Name}");
            Console.WriteLine($"Level:\t{this.Level}");
            Console.WriteLine($"Step:\t{this.Step}");
            Console.WriteLine($"ExpectedResults:\t{this.ExpectedResults}");
        }
    }
}

執行後結果:

Id:    1001
ProjectName:    A專案
CreatTime:    11/19/2020 12:00:00 AM
Name:    登入測試
Level:    高
Step:    開啟登入頁面並且登入
ExpectedResults:    登入成功
=================================================
Id:    1001
ProjectName:    B專案
CreatTime:    11/19/2020 12:00:00 AM
Name:    B專案登入測試
Level:    級別高
Step:    開啟登入頁面並且登入
ExpectedResults:    登入成功
=================================================

從結果中可以看出,通過拷貝後A專案的測試用例還是A專案的,B專案的測試用例是B專案的。建立非常方便。

應用場景

原型模式通常適用於以下場景:

  • 類初始化需要消化非常多的資源,這個資源包括資料、硬體資源等。
  • 通過new產生一個物件需要非常繁瑣的資料準備或訪問許可權,則可以使用原型模式。
  • 一個物件需要提供給其他物件訪問,而且各個呼叫者可能都需要修改其值時,可以考慮使用原型模式拷貝多個物件供呼叫者使用。
  • 在實際專案中,原型模式很少單獨出現,一般是和工廠模式一起出現,通過Clone方法建立一個物件,然後由工廠方法提供給呼叫者。

優缺點

優點:

  • 原型模式向客戶隱藏了建立新例項的複雜性
  • 原型模式允許動態增加或較少產品類。
  • 原型模式簡化了例項的建立結構,工廠方法模式需要有一個與產品類等級結構相同的等級結構,而原型模式不需要這樣。
  • 產品類不需要事先確定產品的等級結構,因為原型模式適用於任何的等級結構

缺點:

  • 每個類必須配備一個克隆方法。
  • 配備克隆方法需要對類的功能進行通盤考慮,這對於全新的類不是很難,但對於已有的類不一定很容易,特別當一個類引用不支援序列化的間接物件,或者引用含有迴圈結構的時候。

相關文章