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