
什么是工廠模式?
??工廠方法模式(英語:Factory method pattern)是一種實(shí)現(xiàn)了“工廠”概念的面向?qū)ο?/a>設(shè)計(jì)模式。就像其他創(chuàng)建型模式一樣,它也是處理在不指定對象具體類型的情況下創(chuàng)建對象的問題。工廠方法模式的實(shí)質(zhì)是“定義一個創(chuàng)建對象的接口,但讓實(shí)現(xiàn)這個接口的類來決定實(shí)例化哪個類。工廠方法讓類的實(shí)例化推遲到子類中進(jìn)行?!?sup>[1]
??創(chuàng)建一個對象常常需要復(fù)雜的過程,所以不適合包含在一個復(fù)合對象中。創(chuàng)建對象可能會導(dǎo)致大量的重復(fù)代碼,可能會需要復(fù)合對象訪問不到的信息,也可能提供不了足夠級別的抽象,還可能并不是復(fù)合對象概念的一部分。工廠方法模式通過定義一個單獨(dú)的創(chuàng)建對象的方法來解決這些問題。由子類實(shí)現(xiàn)這個方法來創(chuàng)建具體類型的對象。
??對象創(chuàng)建中的有些過程包括決定創(chuàng)建哪個對象、管理對象的生命周期,以及管理特定對象的創(chuàng)建和銷毀的概念。
認(rèn)識工廠模式
??當(dāng)使用new時,就會實(shí)例化一個具體類,而代碼綁著具體類會導(dǎo)致代碼更脆弱,更缺乏彈性。因此需要使用接口來讓代碼具有彈性,但還是得建立具體類(Student)的實(shí)例,如下面的代碼:
People people = new Student();
??當(dāng)有一群相關(guān)的具體類時,通常會寫出下面的代碼:
People people;
if(study){
people = new Student();
}else if(teach){
people = new Teacher();
}else if(work){
people = new Worker();
}
??這里有一些要實(shí)例化的具體類,究竟該實(shí)例化哪個類,要在運(yùn)行時由一些條件來決定。
??當(dāng)這樣的代碼一旦有變化或擴(kuò)展,就必須重新打開這段代碼進(jìn)行檢查和修改。通常這樣修改過的代碼將造成部分系統(tǒng)更難維護(hù)和更新,而且也更容易犯錯。
??new有什么不對勁呢?
??在技術(shù)上,new沒有錯,畢竟這是Java的基礎(chǔ)定義。真正要說的是“改變”,以及它是如何影響new的使用的。
??針對接口編程,可以隔離掉以后系統(tǒng)可能發(fā)生的一大堆改變。為什么呢?如果代碼是針對接口而寫,那么通過多態(tài),它可以與任何新類實(shí)現(xiàn)該接口。但是,當(dāng)代碼使用大量的具體類時,等于是自找麻煩,因?yàn)橐坏┘尤胄碌木唧w類,就必須改變代碼。也就是說,你的代碼并非“對修改關(guān)閉”。想要使用新的具體類型來擴(kuò)展代碼,必須重新打開它。這就是個問題了,因此我們應(yīng)當(dāng)找出“變化”的代碼,將其從“不變”的代碼中分離出來。
一個例子開始了解工廠模式
??假如你要開一個包子店,你首先需要寫一個包子類Bun來描述包子的詳細(xì)信息,各種包子都要繼承這個父類,該類代碼如下:
public abstract class Bun {
String name;//包子名稱
String dough;//面團(tuán)種類
String stuffing;//餡的種類
//準(zhǔn)備階段
void prepare() {
System.out.println("將面粉和酵母水混合攪拌捏成團(tuán)");
}
//發(fā)酵階段
void ferment() {
System.out.println("將面團(tuán)蓋上保鮮膜放置一到兩小時");
}
//切片階段
void cut() {
System.out.println("將面團(tuán)揉成長條切成小份,揉成小團(tuán)");
}
//包餡階段
void farci() {
System.out.println("把準(zhǔn)備好的餡放進(jìn)面團(tuán)中間,再整理好形狀");
}
//清蒸階段
void steam() {
System.out.println("將包子放進(jìn)蒸籠清蒸");
}
//打包階段
void box() {
System.out.println("將蒸完的包子打包好");
}
public String getName() {
return name;
}
}
??有了基本的包子類,你就可以賣包子了,所以還需要一個包子店鋪類BunStore,代碼如下:
public class BunStore {
//訂購包子
public Bun orderBun(){
Bun bun = new Bun();
bun.prepare();
bun.ferment();
bun.cut();
bun.farci();
bun.steam();
bun.box();
return bun;
}
}
??但是包子的種類不可能是單一的,所以要作出一些變化,而且為了使系統(tǒng)更有彈性,我們的Bun類應(yīng)該是一個抽象類或接口(開始我們就這么定義的),改變后的代碼:
public class BunStore {
//訂購包子
public Bun orderBun(String type) {
Bun bun = null;
//具體包子種類的英文太長,此處用拼音代替
if (type.equals("三鮮")) {
bun = new SanXianBun();
} else if (type.equals("蛋黃")) {
bun = new DanHuangBun();
} else if (type.equals("豆沙")) {
bun = new DouShaBun();
} else {
return null;
}
bun.prepare();
bun.ferment();
bun.cut();
bun.farci();
bun.steam();
bun.box();
return bun;
}
}
??但是作為一個小商人,肯定會關(guān)注包子的銷量情況,如果豆沙包賣的不好,可能將其下架換上另一種類的包子。而且有些餡的包子是不同季節(jié)才有的。因此,我們上面的代碼可能頻繁地變化(if語句的內(nèi)容頻繁增加刪除)。
??“變化”?那就像策略模式一樣,將“變化”的代碼抽離出來封裝不就行了。其中包子的加工流程是不怎么變化的,變化的是訂購包子的種類,將其抽離出變?yōu)橐粋€類,這個新對象就叫工廠:
public class SimpleBunFactory {
public Bun createBun(String type){
Bun bun = null;
//具體包子種類的英文太長,此處用拼音代替
if (type.equals("三鮮")) {
bun = new SanXianBun();
} else if (type.equals("蛋黃")) {
bun = new DanHuangBun();
} else if (type.equals("豆沙")) {
bun = new DouShaBun();
} else {
return null;
}
return bun;
}
}
??該工廠處理創(chuàng)建對象的細(xì)節(jié),之后修改下原來的BunStore代碼:
public class BunStore {
SimpleBunFactory factory;
//將工廠作為參數(shù)傳入構(gòu)造器
public BunStore(SimpleBunFactory factory) {
this.factory = factory;
}
//訂購包子
Bun orderBun(String type) {
Bun bun = factory.createBun(type);
bun.prepare();
bun.ferment();
bun.cut();
bun.farci();
bun.steam();
bun.box();
return bun;
}
}
??問:這樣做有什么好處?似乎只是把問題搬到另一個對象罷了,問題仍然存在。
??答:``SimpleBunFactory該工廠可以有許多客戶,現(xiàn)在是只有一個orderBun方法是它的客戶,但未來可能還有BunShopMenu(包子店菜單)類,會利用這個工廠來取得包子的價錢和描述,或者其他更多的客戶??偠灾?,該工廠可以有許多的客戶。因此,把創(chuàng)建包子的代碼包裝進(jìn)一個類,當(dāng)以后需要實(shí)現(xiàn)改變時,只需修改這個類即可。
??問:經(jīng)常在代碼中看到把工廠定義為靜態(tài)的方法,這有何差別?
??答:利用靜態(tài)方法定義一個簡單的工廠常被稱作靜態(tài)工廠。為何使用靜態(tài)方法?因?yàn)椴恍枰褂脛?chuàng)建對象的方法來實(shí)例化對象。但也有缺點(diǎn),那就是不能通過繼承來改變創(chuàng)建方法的行為。
定義簡單工廠(非設(shè)計(jì)模式)
??簡單工廠其實(shí)不是一個設(shè)計(jì)模式,反而比較像是一種編程習(xí)慣。雖然它不是一個真正的模式,但了解其用法還是很有必要,讓我們看看新的包子店類圖:

改進(jìn)原有例子
??假如你的包子店經(jīng)營有成,希望在別的地方開加盟店。身為加盟公司經(jīng)營者,不得不考慮不同地域包子風(fēng)味的問題,天津的包子有天津的特色,陜西的包子有陜西的特色。
??如果利用工廠SimpleBunFactory,寫出多種不同的工廠:TianJinBunFactory、ShanXiBunFactory,那么各地的加盟店都有合適的工廠可以使用,代碼如下:
TianJinBunFactory tjFactory = new TianJinBunFactory();
BunStore tjStore = new BunStore();
tjStore.orderBun("三鮮");
ShanXiBunFactory sxFactory = new ShanXiBunFactory();
BunStore sxStore = new BunStore();
sxFactory.orderBun("三鮮");
??在推廣工廠SimpleBunFactory時,你發(fā)現(xiàn)加盟店確實(shí)是用你的工廠創(chuàng)建包子,但是其他部分如包餡階段、清蒸階段可能采用它們自創(chuàng)的做法。你希望能夠建立一個框架,把加盟店和創(chuàng)建包子捆綁在一起的同時又保持一定的彈性,那么如何得“魚”又得“熊掌”呢?
??我們可以這樣做,把createBun方法放到BunStore中,不過要把它設(shè)置為“抽象方法”,然后為不同地域的加盟店創(chuàng)建不同的BunStore的子類。首先看看BunStore的改變:
public abstract class BunStore {
//訂購包子
public final Bun orderBun(String type) {
//從工廠對象移回BunStore
Bun bun = createBun(type);
bun.prepare();
bun.ferment();
bun.cut();
bun.farci();
bun.steam();
bun.box();
return bun;
}
//把工廠對象移到這個方法中,該方法是抽象的
protected abstract Bun createBun(String type);
}
??現(xiàn)在有了一個BunStore作為父類,讓每個加盟店(天津包子鋪,陜西包子鋪)子類都繼承自這個BunStore,每個子類各自決定如何制造包子,讓我們看看現(xiàn)在的類圖:

??
問:``BunStore的子類終究是子類,如何做決定?而且子類TianJinBunStore也沒有看到任何做決定的邏輯代碼啊。??
答:這個應(yīng)該從BunStore類的orderBun()方法來看,此方法在抽象的BunStore類中定義,但是只在子類中實(shí)現(xiàn)具體類型。
??現(xiàn)在更進(jìn)一步地,
orderBun()方法對Bun對象做了許多事情(準(zhǔn)備、發(fā)酵等等),但由于Bun是抽象的,orderBun()方法并不知道哪些具體類參與進(jìn)來了,換句話說,這就是解耦。
??如上圖,BunStore對象通過orderBun()方法調(diào)用createBun()方法取得包子對象,但究竟會取得哪一種包子對象呢?這不是由orderBun()方法所能決定的,那么究竟是誰決定呢?當(dāng)然是具體的包子鋪(TianJinBunStore、ShanXiBunStore)來決定啦。
??那么,這些包子店子類是實(shí)時做出這樣的決定嗎?不是的,但從orderBun()方法的角度來看,如果選擇在TianJinBunStore這個子類店訂購包子,則由這個子類店來決定。嚴(yán)格來說,并非由這個子類店實(shí)際做決定,而是看顧客選擇哪個包子店,此時才決定了包子的風(fēng)味。
??下面是TianJinBunStore的代碼:
public class TianJinBunStore extends BunStore {
@Override
protected Bun createBun(String type) {
Bun bun = null;
//具體包子種類的英文太長,此處用拼音代替
if (type.equals("三鮮")) {
bun = new TianJinSanXianBun();
} else if (type.equals("蛋黃")) {
bun = new TianJinDanHuangBun();
} else if (type.equals("豆沙")) {
bun = new TianJinDouShaBun();
} else {
return null;
}
return bun;
}
}
//天津口味的不同類型的包子
public class TianJinDouShaBun extends Bun {
public TianJinDouShaBun() {
name = "天津風(fēng)味的豆沙包";
dough = "普通的面團(tuán)";
stuffing = "豆沙餡";
}
}
public class TianJinSanXianBun extends Bun {
public TianJinSanXianBun() {
name = "天津風(fēng)味的三鮮包";
dough = "上等面團(tuán)";
stuffing = "豬肉芹菜餡";
}
}
public class TianJinDanHuangBun extends Bun {
public TianJinDanHuangBun() {
name = "天津風(fēng)味的蛋黃包";
dough = "勁道的面團(tuán)";
stuffing = "蛋黃餡";
}
}
??可以看到,TianJinBunStore類繼承自BunStore類,創(chuàng)建的包子都是天津口味的,陜西包子鋪的代碼類似。
??我們在回頭講解一下BunStore代碼:
public abstract class BunStore {
//訂購包子
public final Bun orderBun(String type) {
//從工廠對象移回BunStore
Bun bun = createBun(type);
bun.prepare();
bun.ferment();
bun.cut();
bun.farci();
bun.steam();
bun.box();
return bun;
}
//把工廠對象移到這個方法中,該方法是抽象的
protected abstract Bun createBun(String type);
}
??原本是由一個對象負(fù)責(zé)所有具體類的實(shí)例化,現(xiàn)在通過對BunStore類做的改變,變成由一堆子類來負(fù)責(zé)實(shí)例化。而且,現(xiàn)在實(shí)例化包子的則類被轉(zhuǎn)移到createBun()方法中,此方法就如同一個工廠。
??工廠方法用來處理對象的創(chuàng)建,并將這樣的行為封裝在子類中。這樣,客戶程序中關(guān)于超類的代碼就和子類對象創(chuàng)建代碼解耦了:

測試?yán)?/h4>
??終于到了測試包子店的時候了,訂購包子的流程如下:
①首先需要實(shí)例化一個地區(qū)加盟的包子店;
②然后在這個店里訂購相應(yīng)種類的包子;
③之后就可以獲取訂購到的包子的詳細(xì)信息了。
BunStore tjStore = new TianJinBunStore();
Bun bun = tjStore.orderBun("豆沙");
System.out.println("有顧客訂購了" + bun.getName());
??測試結(jié)果如下:
將面粉和酵母水混合攪拌捏成團(tuán)
將面團(tuán)蓋上保鮮膜放置一到兩小時
將面團(tuán)揉成長條切成小份,揉成小團(tuán)
把準(zhǔn)備好的餡放進(jìn)面團(tuán)中間,再整理好形狀
將包子放進(jìn)蒸籠清蒸
將蒸完的包子打包好
有顧客訂購了天津風(fēng)味的豆沙包
了解工廠模式
??沒錯,上面這些類又、用到了工廠模式。
??所有工廠模式都用來封裝對象的創(chuàng)建。工廠方法模式通過讓子類決定該創(chuàng)建的對象是什么,來達(dá)到將對象創(chuàng)建的過程封裝的目的。我們來看看它們的類圖:

??可以看到,將一個
orderBun()方法和一個工廠方法聯(lián)合起來,就可以成為一個框架。除此之外,工廠方法將生產(chǎn)知識封裝進(jìn)各個創(chuàng)建者,這樣的做法,也可以被視為一個框架:
定義工廠方法模式
??下面是工廠方法模式的正式定義:
??工廠方法模式:定義了一個創(chuàng)建對象的接口,但由子類決定要實(shí)例化的類是哪一個,工廠方法讓類把實(shí)例化推遲到子類。
??工廠方法模式能夠封裝具體類型的實(shí)例化。如下面的類圖,抽象的Creator提供了一個創(chuàng)建對象的方法的接口,也稱為“工廠方法”。在抽象的Creator中,任何其他實(shí)現(xiàn)的方法,都可能使用到這個工廠方法所制造出來的產(chǎn)品,但只有子類真正實(shí)現(xiàn)這個工廠方法并創(chuàng)建產(chǎn)品。

??經(jīng)常有開發(fā)人員說:工廠方法讓子類決定要實(shí)例化的類是哪一個。希望不要理解錯誤,所謂的“決定”,并不是指模式允許子類本身在運(yùn)行時做決定,而是指在編寫創(chuàng)建者類時,不需要知道實(shí)際創(chuàng)建的產(chǎn)品是哪一個。選擇了使用哪個子類,自然就決定了實(shí)際創(chuàng)建的產(chǎn)品是什么。
疑問解答
??問:當(dāng)只有一個ConcreteCreator的時候,工廠方法模式有什么優(yōu)點(diǎn)?
??答:盡管只有一個具體創(chuàng)建者,工廠方法模式依然很有用,因?yàn)樗鼛椭覀儗a(chǎn)品的“實(shí)現(xiàn)”從“使用”中解耦。如果增加產(chǎn)品或者改變產(chǎn)品的實(shí)現(xiàn),Creator并不會收到影響,因?yàn)樗鼈儍烧咧苯佣疾皇蔷o耦合。
??問:如果說天津包子店是利用簡單工廠創(chuàng)建的,這樣的說法是否正確?看起來很像。
??答:不正確。它們很類似,但用法不同。雖然每個具體商店的實(shí)現(xiàn)看起來都很像是SimpleBunFactory,但別忘了,工廠方法模式里的具體實(shí)現(xiàn)是擴(kuò)展自一個BunStore類,此類有一個抽象方法createBun(),由每個商店自行負(fù)責(zé)createBun()方法的行為。而在簡單工廠中,工廠是另一個由BunStore使用的對象。
??問:工廠方法和創(chuàng)建者是否總是抽象的?
??答:不是的,可以定義一個默認(rèn)的工廠方法來產(chǎn)生某些具體的產(chǎn)品,這么一來,即使創(chuàng)建者沒有任何子類,依然可以創(chuàng)建產(chǎn)品。
??問:每個商店基于傳入的參數(shù)制造出不同種類的包子。是否所有的具體創(chuàng)建者都必須如此?能不能只創(chuàng)建一種包子?
??答:我們采用的方式稱為“參數(shù)化工廠方法”,它可以根據(jù)傳入的參數(shù)創(chuàng)建不同的對象。但是工廠經(jīng)常只產(chǎn)生一種對象,所以此時可以不需要參數(shù)化。工廠模式的這兩種形式都是有效的。
??問:簡單工廠和工廠方法之間的差異令人很困惑,看起來很類型,差別在于,在工廠方法中,返回包子的類是子類,如何解釋?
??答:子類的確看起來很像簡單工廠,而簡單工廠把全部的事情在一個地方都處理完成,然而工廠方法卻是創(chuàng)建一個框架,讓子類決定要如何實(shí)現(xiàn)。比方說,在工廠方法中,orderBun()方法提供了一般的框架,以便創(chuàng)建包子,orderBun()依賴于工廠方法創(chuàng)建具體類,并制造出實(shí)際的包子??赏ㄟ^繼承自BunStore類,決定實(shí)際制造出的包子是什么。簡單工廠的做法,可以將對象的創(chuàng)建封裝起來,但是簡單工廠不具備工廠方法的彈性,因?yàn)楹唵喂S不能變更正在創(chuàng)建的產(chǎn)品。
??問:那么這些所謂的“工廠”究竟能帶來什么好處?
??答:有許多好處。將創(chuàng)建對象的代碼集中在一個對象或方法中,可以避免代碼中的重復(fù),并且更方便以后的維護(hù)。這也意味著客戶在實(shí)例化對象時,只依賴于接口,而不是具體類。針對接口編程,可以讓代碼更具有彈性,應(yīng)對未來的擴(kuò)展。
參考資料
《HeadFirst設(shè)計(jì)模式》