正如同筆者在<簡單工廠模式>一節裡介紹的,工廠模式有簡單工廠模式,工廠方法模式和抽象工廠模式幾種形態。簡單工廠模式已經在前面作過介紹。在簡單工廠模式中,一個工廠類處於對產品類實例化調用的中心位置上,它決定那一個產品類應當被實例化, 如同一個交通警察站在來往的車輛流中,決定放行那一個方向的車輛向那一個方向流動一樣。
而本節要討論的工廠方法模式是簡單工廠模式的進一步抽象化和推廣。它比簡單工廠模式聰明的地方在於, 它不再作為一個具體的交通警察的面貌出現,而是以交通警察局的面貌出現。它把具體的車輛交通交給下面去管理。換言之,工廠方法模式裡不再只由一個工廠類決定那一個產品類應當被實例化,這個決定被交給子類去作。處於工廠方法模式的中心位置上的類甚至都不去接觸那一個產品類應當被實例化這種細節。這種進一步抽象化的結果,是這種新的模式可以用來處理更加復雜的情形。
為什麼需要工廠方法模式
現在,讓我們繼續考察我們的小花果園。在<簡單工廠模式>一節裡,我們在後花園裡引進了水果類植物, 構造了簡單工廠模式來處理, 使用一個FruitGardener類來負責創立水果類的實例。見下圖。
圖1. 簡單工廠模式。FruitGardener掌握所有水果類的生殺大權。
在這一節裡,我們准備再次引進蔬菜類植物,比如
西紅柿 (Tomato)
土豆 (Potato)
西芥蘭花 (Broccoli)
蔬菜與花和水果當然有共同點,可又有不同之處。蔬菜需要噴灑(dust)殺蟲劑(pesticide)除蟲, 不同的蔬菜需要噴灑不同的殺蟲劑,等等。怎麼辦呢?
那麼,再借用一下簡單工廠模式不就行了? 再設計一個專管蔬菜類植物的工廠類,比如
圖2. 簡單工廠模式。VeggieGardener掌握所有蔬菜類的生殺大權
這樣做一個明顯的不足點就是不夠一般化和抽象化。在FruitGardener和VeggieGardener類之間明顯存在很多共同點, 這些共同點應當抽出來一般化和框架化。這樣一來,如果後花園的主人決定再在園子裡引進些樹木類植物時, 我們有框架化的處理方法。本節所要引入的工廠方法模式就符合這樣的要求。
簡單工廠模式的回顧
有必要首先回顧一下簡單工廠模式的定義,以便於比較。
圖3. 簡單工廠模式的類圖定義
從上圖可以看出,簡單工廠模式涉及到以下的角色
工廠類 (Creator)
擔任這個角色的是工廠方法模式的核心,是與應用程序緊密相關的,直接在應用程序調用下,創立產品實例的那個類。
工廠類只有一個,而且是實的。見下面的位圖
產品 (Product)
擔任這個角色的類是工廠方法模式所創立的對象的父類,或它們共同擁有的接口。
實產品 (Concrete Product)
擔任這個角色的類是工廠方法模式所創立的任何對象所屬的類。
實產品類可以是分布在一維數軸上的分立點 1,2,3,...中的任何一個,見下面的位圖
工廠方法模式的定義
我們給出工廠方法模式的類圖定義如下。
圖4. 工廠方法模式的類圖定義
從上圖可以看出,工廠方法模式涉及到以下的角色
抽象工廠接口(Creator)
擔任這個角色的是工廠方法模式的核心,它是與應用程序無關的。任何在模式中創立對象的工廠類必須實現這個接口。
實工廠類 (Conrete Creator)
擔任這個角色的是與應用程序緊密相關的,直接在應用程序調用下,創立產品實例的那樣一些類。
實工廠類可以是分布在一維數軸上的分立點 1,2,3,...中的任何一個,見下面的位圖
產品 (Product)
擔任這個角色的類是工廠方法模式所創立的對象的父類,或它們共同擁有的接口。
實產品 (Concrete Product)
擔任這個角色的類是工廠方法模式所創立的任何對象所屬的類。
實產品類可以是分布在二維平面上的分立點 (1,1), (1,2), (2,3),...中的任何一個,見下面的位圖
由實工廠1(橫數軸上第一點)創立的對象來自實產品類(1,1), (1,2), (1,3),...。由實工廠2(橫數軸上第二點)創立的對象來自實產品類(2,1), (2,2), (3,3),...。依此類推
工廠方法模式和簡單工廠模式在定義上的不同是很明顯的。工廠方法模式的核心是一個抽象工廠類,而不像簡單工廠模式, 把核心放在一個實類上。工廠方法模式可以允許很多實的工廠類從抽象工廠類繼承下來, 從而可以在實際上成為多個簡單工廠模式的綜合,從而推廣了簡單工廠模式。
反過來講,簡單工廠模式是由工廠方法模式退化而來。設想如果我們非常確定一個系統只需要一個實的工廠類, 那麼就不妨把抽象工廠類合並到實的工廠類中去。而這樣一來,我們就退化到簡單工廠模式了。
與簡單工廠模式中的情形一樣的是,ConcreteCreator 的factory() 方法返還的數據類型是一個接口 PlantIF,而不是哪一個具體的產品類。這種設計使得工廠類創立哪一個產品類的實例細節完全封裝在工廠類內部。
工廠方法模式又叫多形性工廠模式,顯然是因為實工廠類都有共同的接口,或者都有共同的抽象父類。
工廠方法模式在小花果園系統中的實現
好了,現在讓我們回到小花果園的系統裡,看一看怎樣發揮工廠方法模式的威力,解決需要接連不斷向小花果園引進不同類別的植物所帶來的問題。
首先,我們需要一個抽象工廠類,比如叫做 Gardener,作為兩個實工廠類 FruitGardener 及 VeggieGardener 的父類。 Gardener 的 factory() 方法可以是抽象的,留給子類去實現,也可以是實的,在父類實現一部分功能,再在子類實現剩余的功能。我們選擇將 factory() 做成抽象的,主要是因為我們的系統是一個示范系統,內容十分簡單,沒有要在父類實現的任何功能。
圖5. 工廠方法模式在小花果園系統中的實現
抽象工廠類 Gardener 是工廠方法模式的核心,但是它並不掌握水果類或蔬菜類的生殺大權。相反地,這項權力被交給子類,即 VeggieGardener 及 FruitGardener。
package com.javapatterns.factorymethod;
abstract public class Gardener
{
public abstract PlantIF factory(String which) throws BadFruitException;
}
代碼清單1. 父類 Gardener。
package com.javapatterns.factorymethod;
public class VeggieGardener extends Gardener
{
public PlantIF factory(String which) throws BadPlantException
{
if (which.equalsIgnoreCase("tomato"))
{
return new Tomato();
}
else if (which.equalsIgnoreCase("potato"))
{
return new Potato();
}
else if (which.equalsIgnoreCase("broccoli"))
{
return new Broccoli();
}
else
{
throw new BadPlantException("Bad veggie request");
}
}
}
代碼清單2. 子類 VeggieGardener。
package com.javapatterns.factorymethod;
public class FruitGardener extends Gardener
{
public PlantIF factory(String which)
{
if (which.equalsIgnoreCase("apple"))
{
return new Apple();
}
else if (which.equalsIgnoreCase("strawberry"))
{
return new Strawberry();
}
else if (which.equalsIgnoreCase("grape"))
{
return new Grape();
}
else
{
throw new BadPlantException("Bad fruit request");
}
}
}
代碼清單3. 子類 FruitGardener。
package com.javapatterns.factorymethod;
public class Broccoli implements VeggieIF, PlantIF
{
public void grow()
{
log("Broccoli is growing...");
}
public void harvest()
{
log("Broccoli has been harvested.");
}
public void plant()
{
log("Broccoli has been planted.");
}
private static void log(String msg)
{
System.out.println(msg);
}
public void pesticideDust(){ }
}
代碼清單4. 蔬菜類 Broccoli。其它的蔬菜類與 Broccoli 相似,因此不再贅述。
package com.javapatterns.factorymethod;
public class Apple implements FruitIF, PlantIF
{
public void grow()
{
log("Apple is growing...");
}
public void harvest()
{
log("Apple has been harvested.");
}
public void plant()
{
log("Apple has been planted.");
}
private static void log(String msg)
{
System.out.println(msg);
}
public int getTreeAge(){ return treeAge; }
public void setTreeAge(int treeAge){ this.treeAge = treeAge; }
private int treeAge;
}
代碼清單5. 水果類 Apple。與<簡單工廠模式>一節裡的Apple類相比,唯一的區別就是多實現了一個接口 PlantIF。其它的水果類與 Apple 相似,因此不再贅述。
package com.javapatterns.factorymethod;
public class BadPlantException extends Exception
{
public BadPlantException(String msg)
{
super(msg);
}
}
代碼清單6. 例外類 BadPlantException。
工廠方法模式應該在什麼情況下使用
既然工廠方法模式與簡單工廠模式的區別很是微妙,那麼應該在什麼情況下使用工廠方法模式,又應該在什麼情況下使用簡單工廠模式呢?
一般來說,如果你的系統不能事先確定那一個產品類在哪一個時刻被實例化,從而需要將實例化的細節局域化,並封裝起來以分割實例化及使用實例的責任時,你就需要考慮使用某一種形式的工廠模式。
在我們的小花果園系統裡,我們必須假設水果的種類隨時都有可能變化。我們必須能夠在引入新的水果品種時,能夠很少改動程序,就可以適應變化以後的情況。因此,我們顯然需要某一種形式的工廠模式。
如果在發現系統只用一個產品類等級(hierarchy)就可以描述所有已有的產品類,以及可預見的未來可能引進的產品類時,簡單工廠模式是很好的解決方案。因為一個單一產品類等級只需要一個單一的實的工廠類。
然而,當發現系統只用一個產品類等級不足以描述所有的產品類,包括以後可能要添加的新的產品類時,就應當考慮采用工廠方法模式。由於工廠方法模式可以容許多個實的工廠類,以每一個工廠類負責每一個產品類等級,因此這種模式可以容納所有的產品等級。
在我們的小花果園系統裡,不只有水果種類的植物,而且有蔬菜種類的植物。換言之,存在不止一個產品類等級。而且產品類等級的數目也隨時都有可能變化。因此,簡單工廠模式不能滿足需要,為解決向題,我們顯然需要工廠方法模式。
關於模式的實現
在實現工廠方法模式時,有下面一些值得討論的地方。
第一丶在圖四的類圖定義中,可以對抽象工廠(Creator) 做一些變通。變通的種類有
抽象工廠(Creator) 不是接口而是抽象類。一般而言,抽象類不提供一個缺省的工廠方法。 這樣可以有效地解決怎樣實例化事先不能預知的類的問題。
抽象工廠(Creator) 本身是一個實類,並提供一個缺省的工廠方法。 這樣當最初的設計者所預見的實例化不能滿足需要時,後來的設計人員就可以用實工廠類的factory() 方法來置換(Override))父類中factory()方法。
第二丶在經典的工廠方法模式中,factory()方法是沒有參量的。在本文舉例時加入了參量,這實際上也是一種變通。
第三丶在給相關的類和方法取名字時,應當注意讓別人一看即知你是在使用工廠模式。
COM技術架構中的工廠方法模式
在微軟(Microsoft)所提倡的COM(Component Object Model)技術架構中, 工廠方法模式起著關鍵的作用。
在COM架框裡,Creator接口的角色是由一個叫作IClassFactory的COM接口來擔任的。而實類ConcreteCreator的角色是由實現IClassFactory接口的類CFactory(見下圖)來擔任的。一般而言,對象的創立可能要求分配系統資源,要求在不同的對象之間進行協調等等。因為IClassFactory的引進,所有這些在對象的創立過程中出現的細節問題, 都可以封裝在一個實現IClassFactory接口的實的工廠類裡面。這樣一來, 一個COM架構的支持系統只需要創立這個工廠類CFactory的實例就可以了。
圖6. 微軟(Microsoft)的COM(Component Object Model)技術架構是怎樣工作的。
在上面的序列活動(Sequence Activity)圖中,用戶端調用COM的庫函數CoCreateInstance。 CoCreateInstance在COM架框中以CoGetClassObject實現。 CoCreateInstance會在視窗系統的Registry裡搜尋所要的部件(在我們的例子中即CEmployee)。如果找到了這個部件,就會加載支持此部件的DLL。當此DLL加載成功後, CoGetClassObject就會調用DllGetClassObject。後者使用new操作符將工廠類CFactory實例化。
下面,DllGetClassObject會向工廠類CFactory搜詢IClassFactory接口,返還給CoCreateInstance。 CoCreateInstance接下來利用IClassFactory接口調用CreateInstance函數。此時,IClassFactory::CreateInstance調用new操作符來創立所要的部件(CEmployee)。此外,它搜詢IEmployee接口。在拿到接口的指針後, CoCreateInstance釋放掉工廠類並把接口的指針返還給客戶端。
客戶端現在就可以利用這個接口調用此部件中的方法了。
EJB技術架構中的工廠方法模式
升陽(Sun Microsystem)倡導的EJB(Enterprise Java Beans)技術架構是一套為Java語言設計的, 用來開發企業規模應用程序的組件模型。我們來舉例看一看EJB架構是怎樣利用工廠方法模式的。請考察下面的序列活動圖。
圖7. 在升陽所提倡的EJB技術架構中, 工廠方法模式也起著關鍵的作用
在上面的圖中,用戶端創立一個新的 Context 對象,以便利用 JNDI 伺服器尋找 EJBObject。在得到這個 Context 對象後,就可以使用 JNDI 名, 比如"Employee", 來拿到 EJB 類 Employee 的 Home 接口。使用 Employee 的 Home 接口,客戶端可以創立 EJB 對象,比如 EJB 類 Employee 的實例 emp, 然後調用 Employee 的各個方法。
// 取到 JNDI naming context
Context ctx = new InitialContext ();
// 利用ctx 索取 EJB Home 接口
EmployeeHome home = (EmployeeHome)ctx.lookup("Employee");
// 利用Home 接口創立一個 Session Bean 對象
// 這裡使用的是標准的工廠方法模式
Employee emp = home.create (1001, "John", "Smith");
// 調用方法
emp.setTel ("212-657-7879");
代碼清單7. EJB架構中,Home接口提供工廠方法以便用戶端可以動態地創立EJB類Employee的實例。
JMS技術架構中的工廠方法模式
JMS定義了一套標准的API,讓Java語言程序能通過支持JMS標准的MOM(Message Oriented Middleware 面向消息的中間伺服器)來創立和交換消息(message)。我們來舉例看一看JMS(Java Messaging Service)技術架構是怎樣使用工廠方法模式的。
圖8. 在JMS技術架構中, 工廠方法模式無處不在
在上面的序列圖中,用戶端創立一個新的 Context 對象,以便利用 JNDI 伺服器尋找 Topic 和 ConnectionFactory 對象。在得到這個 ConnectionFactory 對象後, 就可以利用 Connection 創立 Session 的實例。有了 Session 的實例後,就可以利用 Session 創立 TopicPublisher的實例,並利用Session創立消息實例。
Properties prop = new Properties();
prop.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
prop.put(Context.PROVIDER_URL, "file:C:\temp");
// 取到 JNDI context
Context ctx = new InitialContext(prop);
// 利用ctx 索取工廠類的實例
Topic topic = (Topic) ctx.lookup("myTopic");
TopicConnectionFactory tcf = (TopicConnectionFactory) ctx.lookup("myTCF");
// 利用工廠類創立Connection,這是典型的工廠模式
TopicConnection tCon = tcf.createTopicConnectoin();
// 利用Connection創立Session的實例,又是工廠模式
TopicSession tSess = tCon.createTopicSession(false,
Session.AUTO_ACKNOWLEDGE);
// 利用Session創立Producer的實例,又是工廠模式
TopicPublisher publisher = tSess.createPublisher(topic);
// 利用Session創立消息實例,又是工廠模式
TextMesage msg = tSess.createTextMessage("Hello from Jeff");
//發送消息
publisher.publish(msg);
代碼清單8. JMS架構中,工廠模式被用於創立 Connection, Session, Producer 的實例。
問答題
第1題、在這一節和上一節的類圖中,我注意到Apple類的類圖與Strawberry類的類圖有一點點不同。在Apple類的類圖左上角有一個夾子樣的標識。請問這個標識代表什麼意思。
第2題、在這一節的類圖4中,我注意到 ConcreteProduct 類只出現一次,但實現 Product 接口的類實際上可以有很多。這是否可以用在聯接 Product 和 ConcreteProduct 之間的線旁注上 1,2,... 表示呢? 記得我在UML圖中曾見過這種記號。
第3題、請問在本節的小花果園系統的源代碼清單4裡,Broccoli 類實現兩個接口,VeggieIF 和 PlantIF。只有 PlantIF 才與工廠模式有關。為什麼不把 VeggieIF 接口合並到 PlantIF 接口中去?
第4題、請問在工廠方法模式中,產品(Product) 何時應是抽象類,何時應是接口?
第5題、請問在工廠方法 (factory())中,為什麼要使用 if 語句作過程性判斷來決定創立哪一個產品類,而不使用多形性原則 (Polymorphsm) 來創立產品類?
問答題答案
第1題、Apple類有性質(property),而Strawberry類沒有性質。
一個類的成員變量叫做屬性(attribute)。性質與屬性的區別在於性質是帶著一套取值丶賦值方法的屬性。一個類有了屬性,其類圖左上角就會有一只夾子。有些人認為,一個Java類有了屬性才能被稱做Java豆(Java Bean)。這只夾子就表示這個類是一只豆。
一個企業Java豆,或 EJB (Enterprise JavaBean) 的類圖左上角也會有一只夾子,夾子上面有一個E字以示與普通的Java豆的不同(請見下圖)。
第2題、不能。在圖4中聯接 Product 和 ConcreteProduct 之間的線有兩條,一條表示兩者之間的推廣關系 (即有向上箭頭的),另一條表示兩者之間的關聯關系(即有向下箭頭的)。在推廣關系線旁寫數字沒有意義。在關聯關系線旁寫數字是有意義的,類旁的數字可以表明類的實例的數目。
原來的問題是關於類的數目而不是類的實例的數目,因此是錯的。
沒有任何必要用數字標明這一點,而且UML也不提供這種標記。
第3題、在面向對象的編程,特別是Java語言的編程中,接口常常用來標志一種身份(identity)。 VeggieIF 和 PlantIF 接口代表兩種不同的身份。VeggieIF 表明 Broccoli 類屬於蔬菜類等級, PlantIF 接口表明 Broccoli 類屬於工廠的產品類。
因此,雖然把兩個接口合並起來可能在功能上是行得通的,在原則上是不應鼓勵這樣做的。
第4題、在工廠方法模式中,產品(Product)可以永遠是抽象類。但在一些情形下可儀簡化為接口。
如果所實產品類( Concrete Product) 之間有共同的邏輯,這部分公有的代碼就應當轉移到產品 (Product) 中去,這樣產品就必須是抽象類而不可能是接口。
反過來,如果所實產品類( Concrete Product) 之間沒有任何共同的邏輯,那麼產品(Product)就沒有任何邏輯代碼,它就應當被作為接口,而不是抽象類。但這不是必須的,僅是建議而已。
第5題、多形性原則 (Polymorphism) 是在對象被創立之後才存在的,因此不能使用多形性來創立對象。factory() 方法必然是非常過程性 (procedural)的。