原型模式(轉載)
在閻巨集博士的《JAVA與模式》一書中開頭是這樣描述原型(Prototype)模式的:
原型模式屬於物件的建立模式。通過給出一個原型物件來指明所有建立的物件的型別,然後用複製這個原型物件的辦法創建出更多同類型的物件。這就是選型模式的用意。
原型模式的結構
原型模式要求物件實現一個可以“克隆”自身的介面,這樣就可以通過複製一個例項物件本身來建立一個新的例項。這樣一來,通過原型例項建立新的物件,就不再需要關心這個例項本身的型別,只要實現了克隆自身的方法,就可以通過這個方法來獲取新的物件,而無須再去通過new來建立。
原型模式有兩種表現形式:(1)簡單形式、(2)登記形式,這兩種表現形式僅僅是原型模式的不同實現。
簡單形式的原型模式
這種形式涉及到三個角色:
(1)客戶(Client)角色:客戶類提出建立物件的請求。
(2)抽象原型(Prototype)角色:這是一個抽象角色,通常由一個Java介面或Java抽象類實現。此角色給出所有的具體原型類所需的介面。
(3)具體原型(Concrete Prototype)角色:被複制的物件。此角色需要實現抽象的原型角色所要求的介面。
原始碼
抽象原型角色
public interface Prototype{ /** * 克隆自身的方法 * @return 一個從自身克隆出來的物件 */ public Object clone(); }
具體原型角色
public class ConcretePrototype1 implements Prototype { public Prototype clone(){ //最簡單的克隆,新建一個自身物件,由於沒有屬性就不再複製值了 Prototype prototype = new ConcretePrototype1(); return prototype; } }
public class ConcretePrototype2 implements Prototype { public Prototype clone(){ //最簡單的克隆,新建一個自身物件,由於沒有屬性就不再複製值了 Prototype prototype = new ConcretePrototype2(); return prototype; } }
客戶端角色
public class Client { /** * 持有需要使用的原型介面物件 */ private Prototype prototype; /** * 構造方法,傳入需要使用的原型介面物件 */ public Client(Prototype prototype){ this.prototype = prototype; } public void operation(Prototype example){ //需要建立原型介面的物件 Prototype copyPrototype = prototype.clone(); } }
登記形式的原型模式
作為原型模式的第二種形式,它多了一個原型管理器(PrototypeManager)角色,該角色的作用是:建立具體原型類的物件,並記錄每一個被建立的物件。
原始碼
抽象原型角色
public interface Prototype{ public Prototype clone(); public String getName(); public void setName(String name); }
具體原型角色
public class ConcretePrototype1 implements Prototype { private String name; public Prototype clone(){ ConcretePrototype1 prototype = new ConcretePrototype1(); prototype.setName(this.name); return prototype; } public String toString(){ return "Now in Prototype1 , name = " + this.name; } @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } }
public class ConcretePrototype2 implements Prototype { private String name; public Prototype clone(){ ConcretePrototype2 prototype = new ConcretePrototype2(); prototype.setName(this.name); return prototype; } public String toString(){ return "Now in Prototype2 , name = " + this.name; } @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } }
原型管理器角色保持一個聚集,作為對所有原型物件的登記,這個角色提供必要的方法,供外界增加新的原型物件和取得已經登記過的原型物件。
public class PrototypeManager { /** * 用來記錄原型的編號和原型例項的對應關係 */ private static Map<String,Prototype> map = new HashMap<String,Prototype>(); /** * 私有化構造方法,避免外部建立例項 */ private PrototypeManager(){} /** * 向原型管理器裡面新增或是修改某個原型註冊 * @param prototypeId 原型編號 * @param prototype 原型例項 */ public synchronized static void setPrototype(String prototypeId , Prototype prototype){ map.put(prototypeId, prototype); } /** * 從原型管理器裡面刪除某個原型註冊 * @param prototypeId 原型編號 */ public synchronized static void removePrototype(String prototypeId){ map.remove(prototypeId); } /** * 獲取某個原型編號對應的原型例項 * @param prototypeId 原型編號 * @return 原型編號對應的原型例項 * @throws Exception 如果原型編號對應的例項不存在,則丟擲異常 */ public synchronized static Prototype getPrototype(String prototypeId) throws Exception{ Prototype prototype = map.get(prototypeId); if(prototype == null){ throw new Exception("您希望獲取的原型還沒有註冊或已被銷燬"); } return prototype; } }
客戶端角色
public class Client { public static void main(String[]args){ try{ Prototype p1 = new ConcretePrototype1(); PrototypeManager.setPrototype("p1", p1); //獲取原型來建立物件 Prototype p3 = PrototypeManager.getPrototype("p1").clone(); p3.setName("張三"); System.out.println("第一個例項:" + p3); //有人動態的切換了實現 Prototype p2 = new ConcretePrototype2(); PrototypeManager.setPrototype("p1", p2); //重新獲取原型來建立物件 Prototype p4 = PrototypeManager.getPrototype("p1").clone(); p4.setName("李四"); System.out.println("第二個例項:" + p4); //有人登出了這個原型 PrototypeManager.removePrototype("p1"); //再次獲取原型來建立物件 Prototype p5 = PrototypeManager.getPrototype("p1").clone(); p5.setName("王五"); System.out.println("第三個例項:" + p5); }catch(Exception e){ e.printStackTrace(); } } }
兩種形式的比較
簡單形式和登記形式的原型模式各有其長處和短處。
如果需要建立的原型物件數目較少而且比較固定的話,可以採取第一種形式。在這種情況下,原型物件的引用可以由客戶端自己儲存。
如果要建立的原型物件數目不固定的話,可以採取第二種形式。在這種情況下,客戶端不儲存對原型物件的引用,這個任務被交給管理員物件。在複製一個原型物件之前,客戶端可以檢視管理員物件是否已經有一個滿足要求的原型物件。如果有,可以直接從管理員類取得這個物件引用;如果沒有,客戶端就需要自行復制此原型物件。
Java中的克隆方法
Java的所有類都是從java.lang.Object類繼承而來的,而Object類提供protected Object clone()方法對物件進行復制,子類當然也可以把這個方法置換掉,提供滿足自己需要的複製方法。物件的複製有一個基本問題,就是物件通常都有對其他的物件的引用。當使用Object類的clone()方法來複制一個物件時,此物件對其他物件的引用也同時會被複制一份
Java語言提供的Cloneable介面只起一個作用,就是在執行時期通知Java虛擬機器可以安全地在這個類上使用clone()方法。通過呼叫這個clone()方法可以得到一個物件的複製。由於Object類本身並不實現Cloneable介面,因此如果所考慮的類沒有實現Cloneable介面時,呼叫clone()方法會丟擲CloneNotSupportedException異常。
克隆滿足的條件
clone()方法將物件複製了一份並返還給呼叫者。所謂“複製”的含義與clone()方法是怎麼實現的。一般而言,clone()方法滿足以下的描述:
(1)對任何的物件x,都有:x.clone()!=x。換言之,克隆物件與原物件不是同一個物件。
(2)對任何的物件x,都有:x.clone().getClass() == x.getClass(),換言之,克隆物件與原物件的型別一樣。
(3)如果物件x的equals()方法定義其恰當的話,那麼x.clone().equals(x)應當成立的。
在JAVA語言的API中,凡是提供了clone()方法的類,都滿足上面的這些條件。JAVA語言的設計師在設計自己的clone()方法時,也應當遵守著三個條件。一般來說,上面的三個條件中的前兩個是必需的,而第三個是可選的。
淺克隆和深克隆
無論你是自己實現克隆方法,還是採用Java提供的克隆方法,都存在一個淺度克隆和深度克隆的問題。
- 淺度克隆
只負責克隆按值傳遞的資料(比如基本資料型別、String型別),而不復制它所引用的物件,換言之,所有的對其他物件的引用都仍然指向原來的物件。
- 深度克隆
除了淺度克隆要克隆的值外,還負責克隆引用型別的資料。那些引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深度克隆把要複製的物件所引用的物件都複製了一遍,而這種對被引用到的物件的複製叫做間接複製。
深度克隆要深入到多少層,是一個不易確定的問題。在決定以深度克隆的方式複製一個物件的時候,必須決定對間接複製的物件時採取淺度克隆還是繼續採用深度克隆。因此,在採取深度克隆時,需要決定多深才算深。此外,在深度克隆的過程中,很可能會出現迴圈引用的問題,必須小心處理。
利用序列化實現深度克隆
把物件寫到流裡的過程是序列化(Serialization)過程;而把物件從流中讀出來的過程則叫反序列化(Deserialization)過程。應當指出的是,寫到流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面。
在Java語言裡深度克隆一個物件,常常可以先使物件實現Serializable介面,然後把物件(實際上只是物件的拷貝)寫到一個流裡(序列化),再從流裡讀回來(反序列化),便可以重建物件。
public Object deepClone() throws IOException, ClassNotFoundException{ //將物件寫到流裡 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); //從流裡讀回來 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); }
這樣做的前提就是物件以及物件內部所有引用到的物件都是可序列化的,否則,就需要仔細考察那些不可序列化的物件可否設成transient,從而將之排除在複製過程之外。
淺度克隆顯然比深度克隆更容易實現,因為Java語言的所有類都會繼承一個clone()方法,而這個clone()方法所做的正式淺度克隆。
有一些物件,比如執行緒(Thread)物件或Socket物件,是不能簡單複製或共享的。不管是使用淺度克隆還是深度克隆,只要涉及這樣的間接物件,就必須把間接物件設成transient而不予複製;或者由程式自行創建出相當的同種物件,權且當做複製件使用。
孫大聖的身外身法術
孫大聖的身外身本領如果在Java語言裡使用原型模式來實現的話,會怎麼樣呢?首先,齊天大聖(The Greatest Sage)即TheGreatestSage類扮演客戶角色。齊天大聖持有一個猢猻(Monkey)的例項,而猢猻就是大聖本尊。Monkey類具有繼承自java.lang.Object的clone()方法,因此,可以通過呼叫這個克隆方法來複制一個Monkey例項。
孫大聖本人用TheGreatestSage類代表
public class TheGreatestSage { private Monkey monkey = new Monkey(); public void change(){ //克隆大聖本尊 Monkey copyMonkey = (Monkey)monkey.clone(); System.out.println("大聖本尊的生日是:" + monkey.getBirthDate()); System.out.println("克隆的大聖的生日是:" + monkey.getBirthDate()); System.out.println("大聖本尊跟克隆的大聖是否為同一個物件 " + (monkey == copyMonkey)); System.out.println("大聖本尊持有的金箍棒 跟 克隆的大聖持有的金箍棒是否為同一個物件? " + (monkey.getStaff() == copyMonkey.getStaff())); } public static void main(String[]args){ TheGreatestSage sage = new TheGreatestSage(); sage.change(); } }
大聖本尊由Monkey類代表,這個類扮演具體原型角色:
public class Monkey implements Cloneable { //身高 private int height; //體重 private int weight; //生日 private Date birthDate; //金箍棒 private GoldRingedStaff staff; /** * 建構函式 */ public Monkey(){ this.birthDate = new Date(); this.staff = new GoldRingedStaff(); } /** * 克隆方法 */ public Object clone(){ Monkey temp = null; try { temp = (Monkey) super.clone(); } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { return temp; } } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } public GoldRingedStaff getStaff() { return staff; } public void setStaff(GoldRingedStaff staff) { this.staff = staff; } }
大聖還持有一個金箍棒的例項,金箍棒類GoldRingedStaff:
public class GoldRingedStaff { private float height = 100.0f; private float diameter = 10.0f; /** * 增長行為,每次呼叫長度和半徑增加一倍 */ public void grow(){ this.diameter *= 2; this.height *= 2; } /** * 縮小行為,每次呼叫長度和半徑減少一半 */ public void shrink(){ this.diameter /= 2; this.height /= 2; } }
當執行TheGreatestSage類時,首先建立大聖本尊物件,而後淺度克隆大聖本尊物件。程式在執行時打印出的資訊如下:
可以看出,首先,複製的大聖本尊具有和原始的大聖本尊物件一樣的birthDate,而本尊物件不相等,這表明他們二者是克隆關係;其次,複製的大聖本尊所持有的金箍棒和原始的大聖本尊所持有的金箍棒為同一個物件。這表明二者所持有的金箍棒根本是一根,而不是兩根。
正如前面所述,繼承自java.lang.Object類的clone()方法是淺克隆。換言之,齊天大聖的所有化身所持有的金箍棒引用全都是指向一個物件的,這與《西遊記》中的描寫並不一致。要糾正這一點,就需要考慮使用深克隆。
為做到深度克隆,所有需要複製的物件都需要實現java.io.Serializable介面。
孫大聖的原始碼:
public class TheGreatestSage { private Monkey monkey = new Monkey(); public void change() throws IOException, ClassNotFoundException{ Monkey copyMonkey = (Monkey)monkey.deepClone(); System.out.println("大聖本尊的生日是:" + monkey.getBirthDate()); System.out.println("克隆的大聖的生日是:" + monkey.getBirthDate()); System.out.println("大聖本尊跟克隆的大聖是否為同一個物件 " + (monkey == copyMonkey)); System.out.println("大聖本尊持有的金箍棒 跟 克隆的大聖持有的金箍棒是否為同一個物件? " + (monkey.getStaff() == copyMonkey.getStaff())); } public static void main(String[]args) throws IOException, ClassNotFoundException{ TheGreatestSage sage = new TheGreatestSage(); sage.change(); } }
在大聖本尊Monkey類裡面,有兩個克隆方法,一個是clone(),也即淺克隆;另一個是deepClone(),也即深克隆。在深克隆方法裡,大聖本尊物件(一個拷貝)被序列化,然後又被反序列化。反序列化的物件就成了一個深克隆的結果。
public class Monkey implements Cloneable,Serializable { //身高 private int height; //體重 private int weight; //生日 private Date birthDate; //金箍棒 private GoldRingedStaff staff; /** * 建構函式 */ public Monkey(){ this.birthDate = new Date(); staff = new GoldRingedStaff(); } /** * 克隆方法 */ public Object clone(){ Monkey temp = null; try { temp = (Monkey) super.clone(); } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { return temp; } } public Object deepClone() throws IOException, ClassNotFoundException{ //將物件寫到流裡 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); //從流裡讀回來 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } public GoldRingedStaff getStaff() { return staff; } public void setStaff(GoldRingedStaff staff) { this.staff = staff; } }
可以看到,大聖本尊持有一個金箍棒(GoldRingedStaff)的例項。在大聖複製件裡面,此金箍棒例項是原大聖本尊物件所持有的金箍棒物件的一個拷貝。在大聖本尊物件被序列化和反序列化時,它所持有的金箍棒物件也同時被序列化和反序列化,這使得複製的大聖的金箍棒和原大聖本尊物件所持有的金箍棒物件是兩個獨立的物件。
public class GoldRingedStaff implements Serializable{ private float height = 100.0f; private float diameter = 10.0f; /** * 增長行為,每次呼叫長度和半徑增加一倍 */ public void grow(){ this.diameter *= 2; this.height *= 2; } /** * 縮小行為,每次呼叫長度和半徑減少一半 */ public void shrink(){ this.diameter /= 2; this.height /= 2; } }
執行結果:
從執行的結果可以看出,大聖的金箍棒和他的身外之身的金箍棒是不同的物件。這是因為使用了深克隆,從而把大聖本尊所引用的物件也都複製了一遍,其中也包括金箍棒。
原型模式的優點
原型模式允許在執行時動態改變具體的實現型別。原型模式可以在執行期間,由客戶來註冊符合原型介面的實現型別,也可以動態地改變具體的實現型別,看起來介面沒有任何變化,但其實執行的已經是另外一個類例項了。因為克隆一個原型就類似於例項化一個類。
原型模式的缺點
原型模式最主要的缺點是每一個類都必須配備一個克隆方法。配備克隆方法需要對類的功能進行通盤考慮,這對於全新的類來說不是很難,而對於已經有的類不一定很容易,特別是當一個類引用不支援序列化的間接物件,或者引用含有迴圈結構的時候。
個人總結: 原型模式有助於加深對物件的淺拷貝,深拷貝的理解。