設計模式(六)原型模式
一、說說鳴人的影分身
話說鳴人聽了水木老師的建議偷出了卷軸並且學會了一招禁術:影分身之術。當鳴人使用影分身之術的時候就會有好多個和鳴人一模一樣的人出現,就像複製出來的一樣,這種影分身之術在面向物件的設計領域裡就叫做原型模式。
二、什麼是原型模式
有了上邊的鳴人的例子,我們再理解圓形模式的定義應該會更簡單了,GOF給它的定義是:用原型例項指定建立物件的種類並且通過拷貝這些原型物件建立新的物件。
在Java中提供了clone()方法來實現物件的克隆,所以原型模式(Prototype)實現變得簡單的多了。
三、再來說說clone()方法
Java的所有類 都是從java.lang.Object類繼承而來的,而Object類提供下面的方法對物件進行復制:
protected Object clone()
子類也可以將這個方法覆蓋掉,用自己的邏輯實現自己的複製方法。可以被使用clone()方法的類都必須實現Cloneable介面,Cloneable介面只起一個作用就是在執行時期通知Java虛擬機器可以安全地在這個類上使用clone方法。
克隆又分為兩種:淺克隆和深度克隆
淺度克隆:
如上圖所示,淺度複製只是複製物件的值,我們知道物件的屬性一共分為兩種,基本型別和引用型別,對於淺度複製基本型別的資料會複製一份到新的物件中去,對於引用型別的屬性僅僅複製引用的值,引用所指向的具體的物件不會複製,所以A和B實際上是用的同一個物件c,如果再A中改變c的屬性,B中也能看到,因為改變的是兩者共有物件。Java提供的clone方法就是這種型別的。
深度複製與淺度複製的不同就是深度複製不但會複製物件的引用,並且還會複製引用所指的物件。所以在第二幅圖中A和B是指向的不同的物件,此時在A中操作c物件不會對B產生任何影響。
克隆滿足的條件:
1、對任何物件x,都有:x.clone()!=x.換言之,克隆物件與原來的物件不是同一個物件。
2、對任何物件x,都有:x.clone().getClass==x.getClass(),換言之,克隆物件與原物件的型別一致。
3、如果物件x的equals()方法定義恰當的話,那麼x.clone().equals(x)應該是成立的。
關於equals方法的說明:被克隆的物件按照他們的內部狀態是否可變,劃分為可變物件和不可變物件(String的內部數值是不能改變的)。對於可變物件只有當他們是同一個物件時才會返回true,而對於不變物件,當他們的內部狀態值是一樣的時候就認為是true,但是內部狀態一直的不一定就是同一個物件。
四、原型模式的結構
原型模式模式分為兩種,一種是不帶管理類的原型模式,另一種是帶管理類的原型模式。
下面這種是不帶管理類的原型模式:
這種形式涉及到三個角色:
客戶角色:客戶提出建立物件的請求。
抽象原型角色:這是一個抽象角色,通常由一個Java介面或Java抽象類實現,這個類可能會繼承Cloneable介面。
具體原型角色:被複制的物件。此角色需要實現抽象的原型角色所要求的介面。
我們通過一個例項來看一下具體的使用過程。
我們舉一個大學裡常見的例子,一個班裡有一個學霸的話整個班級的作業就不用愁了,大家可以拿學霸的作業去複製嘛。
這個類是作業的抽象父類,定義了一些作業都要實現的方法,這裡只實現了一個數學作業類,將來可以能有程式設計作業等。
package com.designpattern.prototype1;
public abstract class Homework implements Cloneable {
public abstract Object clone();
public abstract void show();
}
數學作業的類要實現自己的複製邏輯,因為數學作業和程式設計作業的抄襲的方法肯定是不一樣的。
package com.designpattern.prototype1;
import java.util.Date;
public class MathHomework extends Homework{
/**
* 這裡只是用一個日期類來表示一下深度複製
*/
private Date A = new Date();
private int a = 1;
public void show() {
System.out.println("Math clone");
}
/**
* 實現自己的克隆方法
*/
public Object clone(){
MathHomework m = null;
/**
* 深度複製
*/
m = (MathHomework) this.clone();
m.A = (Date)this.getA().clone();
return m;
}
public Date getA(){
return A;
}
}
客戶端就可以使用學霸的作業抄襲了
package com.designpattern.prototype1;
public class Main {
public static void main(String[] args){
/**
* 建立一個學霸,全班同學的作業就靠他了
*/
MathHomework xueba = new MathHomework();
/**
* 學渣都是從學霸那複製來的
*/
MathHomework xuezha = (MathHomework)xueba.clone();
xuezha.show();
}
}
那如果一個班裡有兩個學霸呢,那肯定班裡的同學有的會超A同學的,有的會抄B同學的,這樣的話系統裡就必須要保留兩個原型類,這時候使用我們的帶有管理類的原型模式就比較方便了。
此時的結構圖是這樣的:
新增加的管理類:
package com.designpattern.prototype1;
import java.util.Map;
public class Manager {
private static Manager manager;
private Map prototypes = null;
private Manager() {
manager = new Manager();
}
//使用了簡單工廠模式
public static Manager getManager() {
if (manager == null)
manager = new Manager();
return manager;
}
public void put(String name,Homework prototype){
manager.put(name, prototype);
}
public Homework getPrototype(String name){
if(prototypes.containsKey(name)){
return (Homework) ((Homework)prototypes.get(name)).clone();
}else{
Homework homework = null;
try{
homework = (Homework)Class.forName(name).newInstance();
put(name, homework);
}catch(Exception e){
e.printStackTrace();
}
return homework;
}
}
}
package com.designpattern.prototype1;
public class MainManager {
public static void main(String[] args){
/**
* 建立一個學霸,全班同學的作業就靠他了
*/
MathHomework xueba = new MathHomework();
Manager.getManager().put("com.designpattern.prototype1.MathHomework", xueba);
/**
* 學渣都是從學霸那複製來的
*/
MathHomework xuezha = (MathHomework) Manager.getManager().getPrototype("com.designpattern.prototype1.MathHomework");
xuezha.show();
}
}
簡單形式和登記形式的原型模式各有其長處和短處,如果需要建立的原型物件數目較少 而且比較固定的話可以採取簡單形式,如果建立的原型物件數目不固定的話建議採取第二種形式。
五、原型模式的優缺點
優點:
1、將產品的建立過程封裝起來,客戶端不需要了解產品的具體建立流程。
2、利用Java的clone方法來建立物件肯定要比使用new來建立物件快很多,尤其是那些很複雜的物件的時候。
3、可以在不修改其他程式碼的情況下新增新的產品,符合“開-閉”原則。
缺點:原型模式的最大缺點就是每一個類必須都有一個clone方法,如果這個類的組成不太複雜的話還比較好,如果類的組成很複雜的話,如果想實現深度複製就非常困難了。
六、原型模式的選擇
假設一個系統的產品類是動態載入的,而且產品類具有一定的等級結構。這個時候如果採用工廠模式的話,工廠類就不得不具有一個相應的等級結構。而產品類的等級結構一旦發生變化,工廠類的等級結構就不得不有一個相應的變化,這對於產品結構可能經常變化的系統來說採用工廠模式是很不方便的。這個時候如果採用原型模式,給每個產品類裝配一個clone方法便可以避免使用工廠方式所帶來的具有固定等級結構的工廠類。