原型模式與工廠模式的定義,本文不想在這講太多,本文主要想在這講一下對原型模式的一些誤解--將原型模式等價於工廠模式;
爲什麼會產生這種誤導呢?其實也不是我們的錯,關鍵在於設計模式這本書以及網上的其它資料很喜歡將原型和工廠方法進行比較,從而導致我們誤解了原型引入的本質意義。按我的理解,原型引入的根本原因就是在於它可以利用一個原型對象(在這,我指的是實例,而非類),快速地生成一批和原型對象一樣的實例。舉個例子來說,你有一個類A的實例a (A a=new A()),現在你想生成一個和a一樣的實例b,那麼,按照原型的定義,你應該可以這樣做b=a.clone()。這樣,你就可以得到一個和a一模一樣的實例b(即a和部b的數據成員的值完全一樣)。
上面是原型的一個簡單說明,那麼引入原型有什麼好處呢?按我的理解,就是在於:你如果要生成一大批很相像的類的實例時,省得每次去做重複的賦值工作。再舉個例子,如果你有一個類A,它有十個成員變量,現在你打算生成100個A的實例,而這些實例的變量值大部分相同(比如說七個相同),只有一小部分不一樣(比如說三個),那麼如果沒有Prototype,那麼你就得每次New一個A的對像,然後賦值,這樣,你要重複100次同樣的七個變量的賦值工作,顯然,這樣很麻煩。現在你有了原型,那麼問題就簡單了,你只要生成一個A的實例,再通過clone來生成其它的實例,然後再一一修改其它實例不同的地方。
可能我這麼講,大家不信,那下面,再讓我們來看看Java中活生生的原型應用。
學過Java的人都知道,在Java中,有一個clone()函數,這個函數的功能,就是返回一個和當前調用它的對象一樣的實例。那麼Java中爲什麼要引入這個函數呢?在<<Think in Java>>一書中,作者如是解釋:
如果,你要將一個對象的引用作爲參數傳進去,但又不希望函數改變對象的值,那麼,你該怎麼辦?由於在Java中對對象沒有像C++那樣的Const修飾符,所以,爲了實現這個功能,Java中引入了clone函數,使得你將對象的引用作爲參數傳進函數時,這個函數可以調用該對象的Clone方法生成該對象的一份拷貝,從而達到不修改原對象的目的。
我之所以用上面這麼多篇幅來講述原型本質,目的就在於希望各位不要像我一樣,把原型的功能與它的意義給混了,以致於當真正要使用原型來解決問題時,卻不知可以使用它。
好了,上面說了原型的本質意義--至少我認爲是這樣的。那爲什麼很多資料喜歡將原型同工廠模式進行比較呢?不知是不是巧合,雖然原型引入的初衷是像我上面所說,但他實現起來,卻又完全可以達到工廠模式的郊果(後面,我會用代碼實現可以用工廠模式實現的Mac,Win產品系列生成問題)。而且,用起來甚至比工廠模式更方便、靈活。對於工廠模式與原形模式在功能上的這點巧合,我想也許是因爲本來工廠模式和原型模式都是創建型模式,這樣,他們的基本功能都能生成對象,因而使得原型模式在功能上可以代替工廠模式。對這兩種模式在功能上的相同點,程序員2001年第11期雜誌上有一篇”非魚“寫的文章,作者理解得非常巧妙,即:如果你將工廠模式的UMl圖對摺,你得到的就是Prototype原型的UML圖。有興趣比較這兩種模式的朋友,可以去參考這篇文章。
接下來,讓我們在實現機制上來看看原型模式爲什麼可以實現工廠模式的功能(本文只限於Java語言)。在Java中,對於原型的實現,其實根本不用我們做,在object類中早就定義了一個clone函數,而這個函數,就使得我們可以動態地生成對象的當前拷貝。即然這樣,那麼讓我們來看看,如果要實現工廠模式的功能,我
們該如何使用原型模式爲做到呢?
工廠模式實現的生產產品的功能,關鍵是利用了繼承的特性。也就是說,你生成的產品,一定是由同一個抽象產品類派生出來的。所以,在工廠模式下,你如果要生成一類產品,就要引入一個抽像產品類,然後再由它派生出具體產品。同樣,在原型模式中,你完全可以同樣定義一個這樣的“抽象產品--具體產品”層次,再利用具體產品本身的clone功能來產生具體產品本身。從而達到實現工廠模式功能的目的。可能說到這,大家有點糊塗了。實際上,在原型模式中,每個具體產品就扮演了工廠模式裏的具體工廠的角色(爲什麼會這樣,其實很簡單,因爲,每個具體產品都具有生成自己拷貝的功能?從這種意義上講,難道這不正是工廠的作用嗎?)。另外,要在Java中利用原形模式實現工廠模式的功能,則更爲簡單,因爲object已經爲我們實現了clone函數,且對於clone方法,Java中默認是:如果A是父類且A實現了clone函數,B是A的子類,則B不用實現clone函數,它只要調用父類的clone函數,Java就會在運行時動態地爲我們生成正確的B的對象。理解這點的關鍵在於,所有類實現的clone操作都是調用object的clone方法。這也就是說,我上面所說的父類A根本就不用自己實現clone方法,而僅僅是調用父類(object)的clone方法而已。好,到了這,讀者也許又有疑問了,既然所有的cloen操作都是由object實現的,而在java中所有的自定義類默認都是由object派生而來,那這樣的話,應該所有的類都自動就具有了clone自己的能力?
確實,如果object不將它的clone函數聲明爲protect的話,情況的確如此。但Java爲了安全方面的原因,所以沒有將clone方法公開,而是聲明爲保護類型,這樣的話,子類是不可以直接調用object類的clone方法的,而必須做到如下兩點:
1.必須實現Cloneable接口;
2.必須聲明一個clone方法,來調用object的clone函數;
Java在調用父類的clone函數時,都會在運行時動態地進行檢查,如果發現調用的類不符合上面的任何一點,則會拋出一個異常。
明白了上面的原因,那麼如果我們希望某個類具備clone自身的能力,那麼,我們可以這樣做:
1.直接按上面所說,自己實現clone操作;
2.聲明一個抽象父類,實現上面的clone操作並將它聲明爲公開方法,再由此類派生出子類,這樣,所有的子類只要調用父類的clone方法,就能夠正確地拷貝自己。
通常,我們都是使用第一種方式,但在我們現在討論的如何用原型模式實現工廠模式的功能的問題中,我們最好是採用第二種方式。
最後,讓我們通過具體的代碼來看看如何用Prototype模式實現工廠模式的功能。
問題:
現有兩類產品 1-Ram、2—Cpu,現在要生成具體的產品
MacRam、MacCpu和WinRam、WinCpu.
代碼如下:
/**
*A:Abstract
*C:Concrete
*/
/** 定義抽象產品Ram的類 APrototypeRam
* 同時他也是抽象工廠
*/
abstract class APrototypeRam implements Cloneable {
public Object clone() {
Object o=null;
try {
o=super.clone();//調用父類,即Object的clone()
}
catch(CloneNotSupportedException e) {
System.err.println("APrototypeRam is not cloneable!");
}
return o;
}
}
/** 定義抽象產品Ram的類APrototypeProductCpu
* 同時他也是抽象工廠
*/
abstract class APrototypeCpu implements Cloneable {
public Object clone() {
Object o=null;
try {
o=super.clone();//調用父類,即Object的clone()
}
catch(CloneNotSupportedException e) {
System.err.println("APrototypeCpu is not cloneable!");
}
return o;
}
}
/** 定義具體產品MacRam的類CPrototypeMacRam
* 同時他也是具體工廠
*/
class CPrototypeMacRam extends APrototypeRam{
public String toString() {
return "MacRam";
}
}
/** 定義具體產品WinRam的類CPrototypeWinRam
* 同時他也是具體工廠
*/
class CPrototypeWinRam extends APrototypeRam {
public String toString() {
return "WinRam";
}
}
/** 定義具體產品MacCpu的類CPrototypeMacCpu
* 同時他也是具體工廠
*/
class CPrototypeMacCpu extends APrototypeCpu{
public String toString() {
return "MacCpu";
}
}
/** 定義具體產品WinCpu的類CPrototypeWinCpu
* 同時他也是具體工廠
*/
class CPrototypeWinCpu extends APrototypeCpu{
public String toString() {
return "WinCpu";
}
}
/** 客戶端,使用CPrototypeRam和CPrototypeCpu生成如下產品
* MacRam,MacCpu,WinRam,WinCpu
*/
public class Prototype {
public static void main(String[] args) {
/**
* 在生成產品之前,先生成原型產品,以便後面利用它們成批生產相同產品
* 其作用等價於產品工廠
*/
CPrototypeMacRam prototypeMacRam=new CPrototypeMacRam();
CPrototypeWinRam prototypeWinRam=new CPrototypeWinRam();
CPrototypeMacCpu prototypeMacCpu=new CPrototypeMacCpu();
CPrototypeWinCpu prototypeWinCpu=new CPrototypeWinCpu();
CPrototypeMacRam MacRam=(CPrototypeMacRam)prototypeMacRam.clone();
CPrototypeWinRam WinRam=(CPrototypeWinRam)prototypeWinRam.clone();
CPrototypeMacCpu MacCpu=(CPrototypeMacCpu)prototypeMacCpu.clone();
CPrototypeWinCpu WinCpu=(CPrototypeWinCpu)prototypeWinCpu.clone();
System.out.println("打印原形產品與它的克隆產品與比較異同!");
System.out.println("prototypeMacRam:"+prototypeMacRam+" Cloned:"+MacRam);
System.out.println("prototypeWinRam:"+prototypeWinRam+" Cloned:"+WinRam);
System.out.println("prototypeMacCpu:"+prototypeMacCpu+" Cloned:"+MacCpu);
System.out.println("prototypeWinCpu:"+prototypeWinCpu+" Cloned:"+WinCpu);
}
}
通過上面代碼,我們可以清楚地看到,用Prototype模式實現工廠模式更爲簡單,如果再配上原型管理器的話,那麼Prototype模式則會變得更爲靈活,限於篇幅,本文沒有講到原型管理器,有興趣的朋友可以參看後文列出的參考文獻。但同時,我們也發現,使用原形模式時,有一個不足之處,即在客戶端代碼裏,我們必須顯示進行類型轉換,這樣可能導致錯誤。爲了改正這一點,我想,我們可以使用真正的工廠模式將Prototype模式再封裝一遍。對工廠模式的這項功能,恐怕,Prototype原形模式就無能爲力了。
總之,工廠模式和原形模式雖然在引入目的上不同,但在實現上,原形模式可以實現工廠模式同樣的功能。但讀者也不要因爲這樣,而將兩者混爲一體,因爲,反過來,在將原形模式作爲生成本身拷貝的這項功能使用時,工廠模式根本無法取代它。
參考文獻:
1.設計模式--可複用面向對象軟件的基礎 Gof
2.Design Pattern In Java James W. Cooper
3.Think In Java Bruce Eckel
4.程序員2001年第11期雜誌-理解設計模式(2)原型 非魚