C# 設計模式(五)原型模式(unity演示)
1、引言
我們在軟體開發的過程中,常常會用到new欄位來建立物件。那麼是不是隻有這一種辦法來建立物件呢?答案顯然不是的。然而使用new來建立物件時,適用於任何時候呢?顯然答案也是否定的,什麼時候就不適用new呢?讓我們來看看,當我們建立的一個例項的過程很昂貴或者很複雜,並且需要建立多個這樣的類的例項時。如果這時候我們仍然用new操作符去建立這樣的類的例項,會導致記憶體中多分配一個一樣的類例項物件,這未免會增加建立類的複雜度和消耗更多的記憶體空間。那有人會說,採用簡單工廠模式來建立這樣的系統。伴隨著產品類的不斷增加,導致子類的數量不斷增加,反而增加額系統複雜程度,所以在這裡使用共產模式也是來封裝類的建立過程也是不合適的。怎麼辦呢?這裡就不得不提到原型模式了。
2、如何解決
原型模式可以很好地解決這個問題,因為每個類例項都是相同的,當我們需要多個相同的類例項時,沒必要每次都使用new運算子去建立相同的類例項物件,此時我們一般思路就是想——只建立一個類例項物件,如果後面需要更多這樣的例項,可以通過對原來物件拷貝一份來完成建立,這樣在記憶體中不需要建立多個相同的類例項,從而減少記憶體的消耗和達到類例項的複用。 然而這個思路正是原型模式的實現方式。下面就具體介紹下設計模式中的原型設計模式。
3、原型模式詳細介紹
3.1、原型模式的定義
原型模式(Prototype Pattern)
用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。簡單來說就是從一個物件再建立另外一個可定製的物件,而且不需要知道任何的建立細節。在現實生活中,也有很多原型設計模式的例子,例如,細胞分裂的過程,一個細胞的有絲分裂產生兩個相同的細胞;還有西遊記中孫悟空變出後孫的本領和火影忍者中鳴人的隱分身忍術等
3.2、原型模式結構
下面是原型模式的類圖,我們一起來看看!
通過上圖我們不難發現,在原型模式中只有兩個角色:
- 原型 (原型介面Prototype)角色:宣告一個克隆自身的介面;
- 具體原型(具體ConcreteComponent)角色:該類繼承了原型類,用來實現一個克隆自身的操作。
3.3、類圖實現
具體實現程式碼如下所示:
原型類:
/// <summary>
/// 抽象類原型
/// </summary>
public abstract class Prototype
{
private string id;
public string Id
{
get { return id; }
}
public Prototype(string id)
{
this.id = id;
}
/// <summary>
/// 抽象類的關鍵就是這個clone()方法
/// </summary>
/// <returns></returns>
public abstract Prototype Clone();
}
具體原型類
/// <summary>
/// 具體原型
/// </summary>
class ConcretePrototype1 : Prototype
{
public ConcretePrototype1(string id) : base(id)
{
}
/// <summary>
/// 淺複製
/// </summary>
/// <returns></returns>
public override Prototype Clone()
{
//建立當前物件的淺副本
return (Prototype)this.MemberwiseClone();
}
}
(Prototype)this.MemberwiseClone()建立當前物件的淺副本。方法是建立一個新物件,然後將當前物件的非靜態欄位複製到該新物件。
- 如果欄位是值型別的,則對該欄位執行逐位複製。
- 如果是引用型別的,則複製引用,但不復制引用的物件;
因此,原始物件及其副本引用同一物件MSDN。
測試:
ConcretePrototype1 cp1 = new ConcretePrototype1("cp1...");
//克隆類ConcretePrototype1的物件cp1就能得到新的例項c1
ConcretePrototype1 c1 = (ConcretePrototype1)cp1.Clone();
Console.WriteLine("Cloned:{0}", c1.Id);
執行結果:
Cloned:cp1...
3.4、C#舉例
由於克隆類十分常用,以至於.Net在Syste名稱空間中提供了ICloneable介面,其中就只有一個方法Clone(),我們在使用中只需實現這個介面就可以完成原型模式了。下面我們來舉例說明:
3.4.1、情景設定
- 假設一份簡歷,我們需要複製三分,簡歷上顯示:姓名、性別、年齡,工作經歷,工作經歷包括:公司名字和時間區間。
下面是詳細程式碼:
/// <summary>
/// 簡歷類
/// </summary>
public class Resume : ICloneable
{
private string name;
private string sex;
private string age;
private string timeArea;
private string company;
public Resume(string name)
{
this.name = name;
}
/// <summary>
/// 設定個人資訊
/// </summary>
/// <param name="sex"></param>
/// <param name="age"></param>
public void SetPersonalInfo(string sex, string age)
{
this.age = age;
this.sex = sex;
}
/// <summary>
/// 設定工作經歷
/// </summary>
/// <param name="timeArea"></param>
/// <param name="company"></param>
public void SetWorkExperience(string timeArea, string company)
{
this.timeArea = timeArea;
this.company = company;
}
/// <summary>
/// 顯示
/// </summary>
public void Display()
{
Console.WriteLine("{0},{1},{2}",name,sex,age);
Console.WriteLine("{0},{1}",timeArea,company);
}
public object Clone()
{
return (Object)this.MemberwiseClone();
}
}
測試:
Resume p1 = new Resume("大鳥");
p1.SetPersonalInfo("男", "28");
p1.SetWorkExperience("1998-2000", "XX公司");
Resume p2 = (Resume)p1.Clone();
p2.SetWorkExperience("1999-2002", "YY企業");
Resume p3 = (Resume)p1.Clone();
p3.SetPersonalInfo("男", "25");
p1.Display();
p2.Display();
p3.Display();
執行結果:
大鳥,男,28
1998-2000,XX公司
大鳥,男,28
1999-2002,YY企業
大鳥,男,25
1998-2000,XX公司
3.4.2、分析
通過以上程式碼來建立物件時,不需要每次都new一次,這樣大大的提高了效能。一般情況,在初始化的資訊不變的情況下,克隆是最好的辦法。這既隱藏了物件建立的細節,又對效能做了很大的提升。但是上面的是值型別的克隆,那麼對於複雜的引用型別會不會奏效呢?我們把簡歷中的工作經歷改成一個單獨的類,程式碼修改如下:
工作經歷類:
/// <summary>
/// 工作經歷
/// </summary>
public class WorkExperience
{
private string timeArea;
private string company;
public string TimeArea
{
get { return timeArea; }
set { timeArea = value; }
}
public string Company
{
get { return company; }
set { company = value; }
}
}
簡歷類:
/// <summary>
/// 簡歷類
/// </summary>
public class Resume : ICloneable
{
private string name;
private string sex;
private string age;
private WorkExperience work;
public Resume(string name)
{
this.name = name;
work = new WorkExperience();
}
/// <summary>
/// 設定個人資訊
/// </summary>
/// <param name="sex"></param>
/// <param name="age"></param>
public void SetPersonalInfo(string sex, string age)
{
this.age = age;
this.sex = sex;
}
/// <summary>
/// 設定工作經歷
/// </summary>
/// <param name="timeArea"></param>
/// <param name="company"></param>
public void SetWorkExperience(string timeArea, string company)
{
work.TimeArea = timeArea;
work.Company = company;
}
/// <summary>
/// 顯示
/// </summary>
public void Display()
{
Console.WriteLine("{0},{1},{2}",name,sex,age);
Console.WriteLine("{0},{1}",work.TimeArea,work.Company);
}
public object Clone()
{
return (Object)this.MemberwiseClone();
}
}
測試:
Resume p1 = new Resume("大鳥");
p1.SetPersonalInfo("男", "28");
p1.SetWorkExperience("1998-2000", "XX公司");
Resume p2 = (Resume)p1.Clone();
p2.SetWorkExperience("1999-2002", "YY企業");
Resume p3 = (Resume)p1.Clone();
p3.SetPersonalInfo("男", "25");
p3.SetWorkExperience("1998-2000", "ZZ公司");
p1.Display();
p2.Display();
p3.Display();
執行結果:
大鳥,男,28
1998-2000,ZZ公司
大鳥,男,28
1998-2000,ZZ公司
大鳥,男,25
1998-2000,ZZ公司
3.4.3、再次分析
這裡你是否想到了剛才說的,複製的情況又兩種,值型別和引用型別,引用是不同的,可以點選檢視MSDN。這其中提到淺複製,上面第一個例子,可以複製成功是淺複製的原因就是都引用的是值型別。而這裡的是引用型別,對引用的物件還是指向了原來的物件,即只引用了地址。所以造成了工作經歷都是相同的,而且是最後一個。那麼什麼是深複製和淺複製呢?下文解釋。
3.5、深複製與淺複製
- 淺複製:被複制的物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用都仍然指向原來的物件。
- 深複製:把引用物件的變數指向複製過的新物件,而不是原來的被引用的物件。如:我們剛才的例子,我們希望是不同的p1,p2,p3,複製時一變二,二變三。
3.6、深複製使用舉例
還是剛才我們改為深複製:
工作經歷類:
/// <summary>
/// 工作經歷
/// </summary>
public class WorkExperience:ICloneable
{
private string timeArea;
private string company;
public string TimeArea
{
get { return timeArea; }
set { timeArea = value; }
}
public string Company
{
get { return company; }
set { company = value; }
}
public object Clone()
{
return (object)this.MemberwiseClone();
}
}
簡歷類:
/// <summary>
/// 簡歷類
/// </summary>
public class Resume : ICloneable
{
private string name;
private string sex;
private string age;
private WorkExperience work;
private Resume(WorkExperience work)
{
this.work = (WorkExperience)work.Clone();
}
/// <summary>
/// 為Clone()方法呼叫的私有建構函式,以便於克隆“工作經歷”的資料
/// </summary>
/// <param name="name"></param>
public Resume(string name)
{
this.name = name;
work = new WorkExperience();
}
/// <summary>
/// 設定個人資訊
/// </summary>
/// <param name="sex"></param>
/// <param name="age"></param>
public void SetPersonalInfo(string sex, string age)
{
this.age = age;
this.sex = sex;
}
/// <summary>
/// 設定工作經歷
/// </summary>
/// <param name="timeArea"></param>
/// <param name="company"></param>
public void SetWorkExperience(string timeArea, string company)
{
work.TimeArea = timeArea;
work.Company = company;
}
/// <summary>
/// 顯示
/// </summary>
public void Display()
{
Console.WriteLine("{0},{1},{2}",name,sex,age);
Console.WriteLine("{0},{1}",work.TimeArea,work.Company);
}
/// <summary>
///
/// </summary>
/// <returns>最終返回深複製的物件</returns>
public object Clone()
{
//呼叫私有的建構函式,克隆工作經歷,然後再重新給新物件的欄位賦值
Resume obj = new Resume(this.work);
obj.name = this.name;
obj.sex = this.sex;
obj.age = this.age;
return obj;
}
}
測試,我們繼續用上面的,執行結果如下:
大鳥,男,28
1998-2000,XX公司
大鳥,男,28
1999-2002,YY企業
大鳥,男,25
1998-2000,ZZ公司
這次修改是不是達到了預期呢?深複製複製了是新的物件,與原來的物件沒有共享關係。
3.7、原型模式的要點
- 用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。
- 原型模式是一種比較簡單的模式,也非常容易理解,實現一個介面,重寫一個方法即完成了原型模式。在實際應用中,原型模式很少單獨出現。經常與其他模式混用,他的原型類Prototype也常用抽象類來替代。
- 使用原型模式拷貝物件時,需注意淺拷貝與深拷貝的區別。
- 原型模式可以結合JSON等資料交換格式,為資料模型構建原型。
4、原型模式的優缺點
優點:
- 原型模式向客戶隱藏了建立新例項的複雜性
- 原型模式允許動態增加或較少產品類。
- 原型模式簡化了例項的建立結構,工廠方法模式需要有一個與產品類等級結構相同的等級結構,而原型模式不需要這樣。
- 產品類不需要事先確定產品的等級結構,因為原型模式適用於任何的等級結構
缺點:
- 每個類必須配備一個克隆方法
- 配備克隆方法需要對類的功能進行通盤考慮,這對於全新的類不是很難,但對於已有的類不一定很容易,特別當一個類引用不支援序列化的間接物件,或者引用含有迴圈結構的時候。
5、原型模式的適用場景
原型模式適用於:
- 產生物件過程比較複雜,初始化需要許多資源時。
- 希望框架原型和產生物件分開時。
- 同一個物件可能會供其他呼叫者同時呼叫訪問時。
6、應用舉例(unity演示)
也是寫幾個類,這裡就不在演示了!
7、總結
最美好的時光總是短暫的,原型模式的介紹就結束了,原型模式用一個原型物件來指明所要建立的物件型別,然後用複製這個原型物件的方法來創建出更多的同類型物件。它與工廠方法模式的實現非常相似,其中原型模式中的Clone方法就類似工廠方法模式中的工廠方法,只是工廠方法模式的工廠方法是通過new運算子重新建立一個新的物件(相當於原型模式的深拷貝實現),而原型模式是通過呼叫MemberwiseClone方法來對原來物件進行拷貝,也就是複製,同時在原型模式優點中也介紹了與工廠方法的區別。好了一句話,深複製建立的物件與原物件沒有任何共享,一個物件的改變不會影響到另外一個物件;而淺複製是共享的,一個改變了,另外一個物件的成員的值會隨之改變。
The End
好了,今天的分享就到這裡,如有不足之處,還望大家及時指正,隨時歡迎探討交流!!!
喜歡的朋友們,請幫頂、點贊、評論!您的肯定是我寫作的不竭動力!