原型模式(Prototype Pattern),用原型執行個體指定建立對象的種類,并通過拷貝這些原型建立新的對象;即用于建立重複的對象,同時又能保證性能。這種類型的設計模式屬于建立型模式,它提供了一種建立對象的最佳方式。
這種模式是實作了一個原型接口,該接口用于建立目前對象的克隆。當直接建立對象的代價比較大時,則采用這種模式。例如,一個對象需要在一個高代價的資料庫操作之後被建立。我們可以緩存該對象,在下一個請求時傳回它的克隆,在需要的時候更新資料庫,以此來減少資料庫調用。
在運作期建立和删除原型;利用一個已有的原型對象,快速的生成和原型對象一樣的執行個體;
使用場景:
1、當一個系統應該獨立于它的産品建立,構成和表示時。
2、當要執行個體化的類是在運作時刻指定時,例如,通過動态裝載。
3、為了避免建立一個與産品類層次平行的工廠類層次時。
4、當一個類的執行個體隻能有幾個不同狀态組合中的一種時。建立相應數目的原型并克隆它們可能比每次用合适的狀态手工執行個體化該類更友善一些。
執行個體解析:
原型類以及具體原型實作類的代碼在這裡就不在列出了,詳情請看上面的類圖;
建立一個類,從資料庫擷取實體類,并把它們存儲在一個 Hashtable 中。
ShapeCache.java
import java.util.Hashtable;
public class ShapeCache {
private static Hashtable<String, Shape> shapeMap
= new Hashtable<String, Shape>();
public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
//傳回一個shapeId對應的Shape的克隆對象,注意Shape類要實作Cloneable接口
return (Shape) cachedShape.clone();
}
// 簡化操作:不查資料庫了,直接背景添加三種形狀
public static void loadCache() {
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(),circle);
Square square = new Square();
square.setId("2");
shapeMap.put(square.getId(),square);
Rectangle rectangle = new Rectangle();
rectangle.setId("3");
shapeMap.put(rectangle.getId(),rectangle);
}
}
PrototypePatternDemo 使用 ShapeCache 類來擷取存儲在 Hashtable 中的形狀的克隆。
PrototypePatternDemo.java
public class PrototypePatternDemo {
public static void main(String[] args) {
ShapeCache.loadCache();
Shape clonedShape = (Shape) ShapeCache.getShape("1");
System.out.println("Shape : " + clonedShape.getType());
Shape clonedShape2 = (Shape) ShapeCache.getShape("2");
System.out.println("Shape : " + clonedShape2.getType());
Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
System.out.println("Shape : " + clonedShape3.getType());
}
}
輸出結果:
Shape : Circle
Shape : Square
Shape : Rectangle
實作的關鍵:
1、實作克隆操作,在 JAVA 繼承 Cloneable,重寫 clone() 方法來實作對象的淺複制或通過序列化(serializable)的方式來實作深複制。
2、原型模式同樣用于隔離類對象的使用者和具體類型(易變類)之間的耦合關系,它同樣要求這些”易變類”擁有穩定的接口。
原型模式的優點:
通過普通的new方式執行個體化對象時,每new一次就需要執行一次構造函數,如果構造函數的執行時間很長,那麼多次執行程式效率就會大打折扣;一般在初始化的資訊不發生變化的情況下,克隆是最好的辦法,這既隐藏了對象的建立細節,又大大提升了性能。
淺複制 VS 深複制
以大話設計模式裡面的一個經典的履歷的例子來學習這兩個概念:
淺複制
WorkExperience.java
public class WorkExperience{
private string workDate;
private string company;
public string workDate(){
//getter and setter
}
public string company(){
//getter and setter
}
}
Resume.java
public class Resume implements ICloneable{
private string name;
private string sex;
private string age;
private WorkExperience work;//引用工作經曆對象
//構造函數
public Resume(string name){
this.name = name;
work = new WorkExperience();
}
//設定個人資訊
public void setPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設定工作經曆
public void setWorkExperience(String timeArea,String company){
work.timeArea = timeArea;
work.company = company;
}
//顯示
public void display(){
System.out.println("姓名:" + name + " 性别:" + sex + " 年齡:" + age);”
System.out.println("工作經曆:" + work.timeArea +" " + company);
}
//克隆方法
public Object Clone(){
return (Object)this.MemberwiseClone();
}
}
測試類:
public class TestPrototype{
public satic void main(String[] args){
Resume resumeOne = new Resume( "tom");
resumeOne.SetPersonalInfo( "man","22");
resumeOne.SetWorkExperience( "2015-1-2至2015-12-10","IBM");
Resume resumeTwo = (Resume)resumeOne.Clone();
resumeTwo.SetWorkExperience( "2015-1-2至2015-11-11","Microsoft");
Resume resumeThree = (Resume)resumeOne.Clone();
resumeThree.SetPersonalInfo( "man", "32");
resumeThree.SetWorkExperience( "2015-1-2至2015-11-11","甲骨文");
resumeOne.Display();
resumeTwo.Display();
resumeThree.Display();
Console.Read();
}
}
在你的想象裡輸出結果會是什麼呢?我們拿事實來說話:
姓名:tom 性别:man 年齡:22
工作經曆:2015-1-2至2015-11-11 甲骨文
姓名:tom 性别:man 年齡:22
工作經曆:2015-1-2至2015-11-11 甲骨文
姓名:tom 性别:man 年齡:32
工作經曆:2015-1-2至2015-11-11 甲骨文
看到結果有沒有很驚訝,對于工作經曆的輸出相信出乎了好多人的意料,這個地方我們就要說到MemberwiseClone()這個方法了,(有關MemberwiseClone的詳細内容請參見本人的另一篇文章
深複制VS淺複制(MemberwiseClone方法介紹)
),其對于值類型沒有問題,但是對于引用類型,就隻是複制了引用,對引用對象還是指向了原來的對象,是以就會出現我們給resume one two three三處設定工作經曆,但卻得到三個引用都是最後一次的設定,這是因為三個引用都指向了同一對象。
那麼問題來了,到底什麼是淺複制呢?
淺複制:被複制對象的所有變量都含有與原來對象相同的值,而所有的對其他對象的引用都仍然指向原來的對象。
然而我們希望實作的是一變二,二變三,我麼叫這種方式為深複制:把引用對象的變量指向複制過的新對象,而不是原來的被引用的對象。(使用深複制要注意避免循環引用的問題)
深複制如何實作這個例子呢?履歷類和工作經曆類都實作了ICloneable接口;
WorkExperience.java
public class WorkExperience implements ICloneable{
private string workDate;
private string company;
public string workDate(){
//getter and setter
}
public string company(){
//getter and setter
}
//克隆方法
public Object Clone(){
return (Object)this.MemberwiseClone();
}
}
Resume .java
public class Resume implements ICloneable{
private string name;
private string sex;
private string age;
private WorkExperience work;//引用工作經曆對象
//構造函數
public Resume(string name){
this.name = name;
work = new WorkExperience();
}
//注意這個構造方法
private Resume(WorkExperience work){
this.work = work.(WorkExperience)clone;
}
//設定個人資訊
public void setPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設定工作經曆
public void setWorkExperience(String timeArea,String company){
work.timeArea = timeArea;
work.company = company;
}
//顯示
public void display(){
System.out.println("姓名:" + name + "性别:" + sex + "年齡:" + age);
System.out.println("工作經曆:" + work.timeArea + company);
}
//注意差別該類實作的克隆方法與淺複制時實作的克隆方法的差別
public Object Clone(){
//調用私有的構造方法克隆工作經曆,然後再給建立對象指派
Resume resume = new Resume(this.work);
resume.name = this.name;
resume.sex = this.sex;
resume.age = this.age;
}
}
測試類同上,這次輸出結果會是什麼呢?
姓名:tom 性别:man 年齡:22
工作經曆:2015-1-2至2015-12-10 IBM
姓名:tom 性别:man 年齡:22
工作經曆:2015-1-2至2015-11-11 Microsoft
姓名:tom 性别:man 年齡:32
工作經曆:2015-1-2至2015-11-11 甲骨文
由于在一些特定場合,會經常涉及深複制和淺複制,比如說,資料集對象DataSet,他就有Clone()和copy()方法,Clone()方法用來複制DataSet的結構但不複制其資料,實作了原型模式中的淺複制;而copy()方法不但複制其結構還複制其資料,也就是實作了原型模式中的深複制。