引言
在軟件開發過程中,我們習慣使用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方法創建一個對象,然后由工廠方法提供給調用者。
優缺點
優點:
- 原型模式向客戶隱藏了創建新實例的復雜性
- 原型模式允許動態增加或較少產品類。
- 原型模式簡化了實例的創建結構,工廠方法模式需要有一個與產品類等級結構相同的等級結構,而原型模式不需要這樣。
- 產品類不需要事先確定產品的等級結構,因為原型模式適用於任何的等級結構
缺點:
- 每個類必須配備一個克隆方法。
- 配備克隆方法需要對類的功能進行通盤考慮,這對於全新的類不是很難,但對於已有的類不一定很容易,特別當一個類引用不支持串行化的間接對象,或者引用含有循環結構的時候。