-
女媧造人的故事
女媧造人的故事大家都非常熟悉,我們通過這一場景來學(xué)習(xí)工廠模式(例子很好,就是內(nèi)容有點(diǎn)胡扯)。首先,女媧采集黃土捏成人型,然后放到八卦爐中燒制,雖然工藝過程沒錯(cuò),但是意外總會(huì)發(fā)生:
第一次烤泥人,時(shí)間過短,然后白人誕生了;第二次烤,時(shí)間過長,黑人誕生了;第三次,時(shí)間不長不短,黃種人誕生了。
在面向?qū)ο蟮乃季S中,萬物皆對象,是對象我們就可以通過軟件設(shè)計(jì)來實(shí)現(xiàn)。首先對過程進(jìn)行分析,該過程涉及三個(gè)對象:女媧,八卦爐,三種不同膚色的人。女媧可以使用
場景類來表示,八卦爐類似于一個(gè)工廠,負(fù)責(zé)制造生產(chǎn)產(chǎn)品,三種不同膚色的人,他們都是同一個(gè)接口下的不同實(shí)現(xiàn)類,類圖8-1如下:

類圖比較簡單,AbstractHumanFactory是一個(gè)抽象類,定義了一個(gè)八卦爐具有的整體功能,HumanFactory為實(shí)現(xiàn)類,完成具體的任務(wù)——?jiǎng)?chuàng)建人類;Human接口是人類的總稱,其三個(gè)實(shí)現(xiàn)類分別為三類人種;代碼如下:
//人類接口
public interface Human {
void getColor();
}
public class WhiteHuman implements Human {
@Override
public void getColor() {
System.out.println("白人");
}
}
public class BlackHuman implements Human {
@Override
public void getColor() {
System.out.println("黑人");
}
}
public class YellowHuman implements Human {
@Override
public void getColor() {
System.out.println("黃種人");
}
}
//工廠類
public abstract class AbstractHumanFactory {
public abstract <T extends Human> T createHuman(Class<T> c);
}
public class HumanFactory extends AbstractHumanFactory {
@Override
public <T extends Human> T createHuman(Class<T> c) {
Human human = null;
try {
human = (T)Class.forName(c.getName()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return (T) human;
}
}
//場景
public class NvWa {
public static void main(String[] args) {
AbstractHumanFactory humanFactory = new HumanFactory();
Human blackHuman = humanFactory.createHuman(BlackHuman.class);
blackHuman.getColor();
Human whiteHuman = humanFactory.createHuman(WhiteHuman.class);
whiteHuman.getColor();
Human yellowHuman = humanFactory.createHuman(YellowHuman.class);
yellowHuman.getColor();
}
}
以上就是工廠模式。
-
工廠模式定義
工廠模式使用的頻率非常高,在我們?nèi)粘5拈_發(fā)中總能見到他的身影。其定義為:Define an interface for creating an object,but let subclasses decide with class to instantiate. Factory Method lets a class defer instantiation to subclasses(定義一個(gè)工廠用于創(chuàng)建對象的接口,讓子類決定實(shí)例化哪一個(gè)類。工廠方法是一個(gè)類的實(shí)例化延遲到其子類)
工廠方法模式的通用類圖8-2所示:

在工廠方法模式中,抽象產(chǎn)品Product負(fù)責(zé)定義產(chǎn)品的共性,實(shí)現(xiàn)對事物最抽象的定義:Creator 為抽象創(chuàng)建類,也就是抽象工廠,具體如何創(chuàng)建產(chǎn)品類是由具體的實(shí)現(xiàn)工廠ConcreteCreator完成的。工廠方法模式的變種較多,我們來看一個(gè)比較實(shí)用的通用源碼。
//抽象產(chǎn)品
public interface Product {
void method();
}
//具體產(chǎn)品
public class ConcreteProduct implements Product{
@Override
public void method() {
System.out.println("do something");
}
}
//抽象工廠
public abstract class Creator {
public abstract <T extends Product> T FactoryMethod(Class<T> clz);
}
//工廠實(shí)現(xiàn)
public class ConcreteCreator extends Creator{
@Override
public <T extends Product> T FactoryMethod(Class<T> clz) {
Product obj = null;
try {
obj = (Product) Class.forName(clz.getName()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return (T) obj;
}
}
//場景類
public class Client {
public static void main(String[] args) {
Creator creator = new ConcreteCreator();
Product product = creator.FactoryMethod(Concreteproduct.class);
product.method();
}
}
-
工廠方法模式的應(yīng)用
3.1 工廠方法模式的優(yōu)點(diǎn)
首先,良好的封裝性,代碼結(jié)構(gòu)清晰。一個(gè)對象創(chuàng)建是有條件約束的,如一個(gè)調(diào)用者需要一個(gè)具體的產(chǎn)品對象,只要知道這個(gè)產(chǎn)品的類名就可以了,不用知道創(chuàng)建對象的艱辛過程,降低模塊間的耦合。
其次,工廠方法模塊的擴(kuò)展性非常優(yōu)秀。在增加產(chǎn)品類的情況下,只要適當(dāng)?shù)男薷木唧w的工廠類或擴(kuò)展一個(gè)工廠實(shí)現(xiàn)類,就可以完成“擁抱變化”。例如在我們的例子中,需要增加一個(gè)棕色人種,只需要增加一個(gè)brownHuman類,工廠類不用任何修改就可以完成系統(tǒng)擴(kuò)展。
再次,屏蔽產(chǎn)品類。這一特點(diǎn)非常重要,產(chǎn)品類的實(shí)現(xiàn)如何變化,調(diào)用者都不需要關(guān)心,他只需要關(guān)心產(chǎn)品的接口,只要接口保持不變,系統(tǒng)中的上層模塊就不要發(fā)生變化。因?yàn)楫a(chǎn)品類的實(shí)例工作是由工廠負(fù)責(zé)的,一個(gè)產(chǎn)品對象具體由哪一個(gè)產(chǎn)品生成是由工廠類決定的。在數(shù)據(jù)庫開發(fā)中,大家應(yīng)該能夠深刻的體會(huì)到工廠方法模式的好處:如果使用JDBC連接數(shù)據(jù)庫,數(shù)據(jù)庫從Mysql切換到Oracle,需要改動(dòng)的地方就是切換一下驅(qū)動(dòng)名稱(前提條件是sql語句是標(biāo)準(zhǔn)語句),其他的都不需要修改,這是工廠模式靈活性的一個(gè)直接案例。
最后,工廠方法模式是典型的解耦框架。高層模塊值需要知道產(chǎn)品的抽象類,其他的實(shí)現(xiàn)類都不用關(guān)心,符合迪米特法則(最少知識(shí)原則),我不需要的就不去交流;也符合依賴倒置原則,只依賴產(chǎn)品類的抽象;當(dāng)然也符合里氏替換原則,使用產(chǎn)品子類替換產(chǎn)品父類,沒問題!(六大原則指導(dǎo)思想,設(shè)計(jì)模式是六大原則思想的具體實(shí)現(xiàn))
3.2 工廠方法模式的使用場景
首先,工廠方法模式是new一個(gè)對象的替代品,所以在所有需要生成對象的地方都可以使用,但是需要慎重考慮是否要增加一個(gè)工廠類進(jìn)行管理,增加代碼的復(fù)雜度。
其次,需要靈活的、可擴(kuò)展的框架時(shí),可以考慮采用工廠模式。萬物皆對象,那萬物也就皆產(chǎn)品類,例如需要設(shè)計(jì)一個(gè)郵件服務(wù)器的框架,有三種網(wǎng)絡(luò)協(xié)議可供選擇:POP3、IMAP、HTTP,我們就可以把這三中連接方式作為產(chǎn)品類,定義一個(gè)接口如IConnectMail,然后定義對郵件的操作方法,用不同的方法是想三個(gè)具體的產(chǎn)品類(也就是連接方式)再定義一個(gè)工廠類,按照不同的傳入條件,選擇不同的連接方式。如此設(shè)計(jì),可以做到完美的擴(kuò)展,如某些郵件服務(wù)器提供webservice接口,我們只需要增加一個(gè)產(chǎn)品類就可以了。
再次,工廠方法模式可以用在異構(gòu)項(xiàng)目中,例如通過webservice與一個(gè)非java的項(xiàng)目交付,雖然webservice號(hào)稱可以做到異構(gòu)系統(tǒng)的同構(gòu)化,但是在實(shí)踐的開發(fā)中,還是會(huì)碰到很多問題,如類型問題、WSDL文件的支持問題,等等。從WSDL中產(chǎn)生的對象都認(rèn)為是一個(gè)產(chǎn)品,然后由一個(gè)具體的工廠類進(jìn)行管理,減少與外圍系統(tǒng)的耦合。(webservice的接口做得不多,這個(gè)意思是通過定義一個(gè)標(biāo)識(shí)接口,讓外圍系統(tǒng)產(chǎn)生的對象都實(shí)現(xiàn)這個(gè)標(biāo)識(shí)接口去統(tǒng)一管理嗎)
-
工廠方法模式的擴(kuò)展
工廠方法模式有很多擴(kuò)展,而且與其他模式結(jié)合使用威力更大,下面將介紹4種擴(kuò)展。(就我自己理解的一種就是可以把工廠類做成單例的,一般工廠會(huì)涉及很多配置信息,這個(gè)類就比較重,做成單例的就可以減少創(chuàng)建銷毀的開銷)
4.1 縮小為簡單工廠模式
我們這樣考慮一個(gè)問題:一個(gè)模塊僅需要一個(gè)工廠類,沒必要把他產(chǎn)生出來,使用靜態(tài)的方法就好了,根據(jù)這一要求,我們把上例中的AbstractHumanFactory修改一下,類圖8-3:

在類圖中可以看到,去掉了抽象工廠類,同時(shí)把createHuman方法設(shè)置為靜態(tài)類型,簡化了類的創(chuàng)建過程,變更的源碼僅僅是HumanFactory和NvWa類,HumanFactory代碼如下:
public class HumanFactory {
public static <T extends Human> T createHuman(Class<T> clz){
Human human = null;
try {
human = (T)Class.forName(c.getName()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return (T) human;
}
}
這個(gè)比較簡單,就是去掉繼承抽象類,將createHuman方法改為類方法,對于不需要工廠類有其他擴(kuò)展的時(shí)候用這個(gè)方法可以簡化代碼,該模式就是工廠方法模式的弱化,因?yàn)楹唵?,所以叫簡單工廠模式(Simple Factory Pattern),也叫做靜態(tài)工廠模式。在實(shí)際項(xiàng)目中,采用該方法的案例還是比較多的,其缺點(diǎn)是工廠類的擴(kuò)展比較困難,不符合開閉原則,但他任然是一個(gè)非常實(shí)用的設(shè)計(jì)模式。
4.2 升級(jí)為多個(gè)工廠類
當(dāng)我們在做一個(gè)比較復(fù)雜的項(xiàng)目時(shí),經(jīng)常會(huì)遇到初始化一個(gè)對象很耗費(fèi)精力的情況,所有的產(chǎn)品類都放到一個(gè)工廠方法中進(jìn)行初始化會(huì)使代碼結(jié)構(gòu)不清晰。例如,一個(gè)產(chǎn)品類有5個(gè)具體實(shí)現(xiàn),每個(gè)實(shí)現(xiàn)類的初始化(不僅僅是new,初始化包括new一個(gè)對象,并對對象設(shè)置一定得初始值)方法都不相同,如果寫在一個(gè)工廠方法中,勢必會(huì)導(dǎo)致該方法巨大無比,那該怎么辦?(這種創(chuàng)建多種實(shí)例并且初始值不同,常規(guī)思路就是加判斷,在分到對應(yīng)多的具體實(shí)現(xiàn)方法,但是這樣每增加一種產(chǎn)品類就得修改工廠類,這就不符合開閉原則,對修改關(guān)閉)
考慮到需要結(jié)果清晰,我們就為每個(gè)產(chǎn)品 定義一個(gè)創(chuàng)造者,然后由調(diào)用者自己去選擇與哪個(gè)工廠方法聯(lián)系。(這種處理方式就是對修改關(guān)閉,對擴(kuò)展開放,新增一種產(chǎn)品類,就去增加對應(yīng)的工廠類)。我們還是以女媧造人為例,每個(gè)人種都有一個(gè)固定的八卦爐,分別造出黑人,白人,黃種人,修改后的類圖8-4:

這種模式的代碼如下(這里并沒有體現(xiàn)出需要這樣去分工廠類的必要性,只是作為舉例):
//工廠類
public abstract class AbstractHumanFactory {
public abstract Human createHuman();
}
public class BlackHumanFactory extends AbstractHumanFactory {
@Override
public abstract Human createHuman(){
return new BlackHumanFactory();
}
}
...
注意:抽象方法中已經(jīng)不需要傳遞相關(guān)的參數(shù)了,因?yàn)槊恳粋€(gè)具體的工廠都已經(jīng)非常明確自己的職責(zé)了:創(chuàng)建自己負(fù)責(zé)的對象。(但是這種就看有沒有必要去用工廠管理了,要是沒必要可能會(huì)增加過多的工廠導(dǎo)致代碼更復(fù)雜了)
每一個(gè)產(chǎn)品類都對應(yīng)了一個(gè)創(chuàng)建類,好處就是創(chuàng)建類的職責(zé)清晰,而且結(jié)構(gòu)簡單,但是給可擴(kuò)展性和可維護(hù)性帶來了一定得影響。為什么這么說呢?如果要擴(kuò)展一個(gè)產(chǎn)品類就需要?jiǎng)?chuàng)建一個(gè)對應(yīng)的工廠類,這就增加了擴(kuò)展的難度。因?yàn)楣S類和產(chǎn)品類的數(shù)量相同,維護(hù)時(shí)需要考慮兩個(gè)對象之間的關(guān)系。
當(dāng)然,在復(fù)雜的應(yīng)用中一般采用多工廠的方法,然后在增加一個(gè)協(xié)調(diào)類,避免調(diào)用者與各個(gè)子工廠交流,協(xié)調(diào)類的作用是封裝子工廠類,對高層模塊提供統(tǒng)一的訪問接口。(在這里突然想到了反射的一些用處,以前只知道有反射這個(gè)東西,但是為什么要有這種方式,僅僅是為了增加創(chuàng)建實(shí)例或是調(diào)用實(shí)例的方法的方式嗎?按我以前的思維方式就是做if..else或是switch...case等,每增加一個(gè)選項(xiàng)就需要去增加這塊選擇的代碼,比如我新增一個(gè)產(chǎn)品類,我如果去做if判斷類型是否符合條件然后創(chuàng)建實(shí)例,但是用反射的方式,我根本不需要一直去維護(hù)判斷的邏輯,完全可以根據(jù)傳的相關(guān)參數(shù)去用反射去創(chuàng)建對象,這就增加了代碼的擴(kuò)展性,那么spring框架可能也是這樣的邏輯,根據(jù)bean中的類的全量名去用反射的方式創(chuàng)建實(shí)例)
4.3 代替單例模式
我們是不是可以采用工廠方法模式實(shí)現(xiàn)單例模式的功能呢?單例模式的核心要求就是在內(nèi)存中只有一個(gè)對象,通過工廠方法模式也可以只在內(nèi)存中產(chǎn)生一個(gè)對象,類圖8-5:

Singleton定義了一個(gè)private的無參構(gòu)造函數(shù),目的是不允許通過new的方式創(chuàng)建一個(gè)對象,代碼如下:
//單例類
public class Singleton {
private Singleton(){
}
public void doSomething(){
}
}
public class SingletonFactory {
private static Singleton singleton;
static {
try {
Class clz = Class.forName(Singleton.class.getName());
Constructor constructor = clz.getDeclaredConstructor();
constructor.setAccessible(true);
singleton = (Singleton) constructor.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
public Singleton getSingleton(){
return singleton;
}
}
通過獲得類構(gòu)造器,然后設(shè)置訪問權(quán)限,生成一個(gè)對象,然后提供外部訪問,保證內(nèi)存中的對象唯一。當(dāng)然其他類也可以通過反射的方式建立一個(gè)單例對象,確實(shí)如此,但是一個(gè)項(xiàng)目或團(tuán)隊(duì)是由章程和規(guī)范的,何況已經(jīng)提供了一個(gè)獲得單例對象的方法,為什么還要重新創(chuàng)建一個(gè)新對象呢。(按這個(gè)方式去思考,我們常說spirng中默認(rèn)bean是單例的,其實(shí)并不是真的單例類,你也可以自己手動(dòng)去創(chuàng)建,但是我們一般不會(huì)這么做,那么在bean的配置中可以通過設(shè)置去創(chuàng)建單例或是多例,實(shí)際上也是這樣的邏輯去做的,通過beanFactory去管理的bean,這塊之后可以去通過看spring框架的源碼去學(xué)習(xí)一下。以上都是本人通過學(xué)習(xí)設(shè)計(jì)模式想到的一些,不知道是否正確,但是提供了一種思考思路。可以想見,像spring這些優(yōu)秀的框架,其中的設(shè)計(jì)模式的實(shí)際運(yùn)用隨處可見,學(xué)完設(shè)計(jì)模式可以帶著這些設(shè)計(jì)理念去閱讀spring源碼,一定會(huì)非常有幫助)
4.4 延遲初始化
何為延遲初始化(Lazy initialization)?一個(gè)對象被消費(fèi)完畢后,并不立刻釋放,工廠類保持其初始狀態(tài),等待再次被使用。延遲初始化是工廠方法模式的一個(gè)擴(kuò)展應(yīng)用(聽名字,應(yīng)該是一個(gè)典型的空間換時(shí)間的思路,對象消費(fèi)完畢并不是立刻釋放,而是持有一段時(shí)間,下次在需要就直接使用,省去了創(chuàng)建銷毀的開銷,但是持有時(shí)會(huì)占用內(nèi)存),其通用類圖8-6:

ProductFactory負(fù)責(zé)產(chǎn)品類對象的創(chuàng)建工作,并且通過prMap變量產(chǎn)生一個(gè)緩存,對需要再次被重用的對象保留(看完類圖發(fā)現(xiàn),這個(gè)好像就是最基礎(chǔ)的緩存原型吧,用map將最近使用的對象存起來,取的時(shí)候先去map中找,找不到在去創(chuàng)建,然后將新創(chuàng)建的實(shí)例維護(hù)到map中,如果map中已經(jīng)存滿(一般緩存肯定會(huì)有一個(gè)上限,要不然內(nèi)存很容易就不夠用了,動(dòng)不動(dòng)就內(nèi)存溢出,那誰頂?shù)牧耍瑧?yīng)用肯定會(huì)出問題),就將比較久遠(yuǎn)的對象移除,在將新的對象保存到map中(這就是一個(gè)緩存策略的問題了)),代碼如下:
public class ProductFactory {
public static final Map<String,Product> prMap = new HashMap<>();
public static synchronized Product creataProduct(String type) throws Exception{
Product product = null;
if(prMap.containsKey(type)){
product = prMap.get(type);
}else{
if("Product".equals(type)){
product = new Concreteproduct();
}else{
//這里應(yīng)該是創(chuàng)建其他實(shí)現(xiàn)類對應(yīng)的實(shí)例,就不寫了
}
prMap.put(type,product);
}
return product;
}
}
延遲加載框架是可以擴(kuò)展的,例如限制某一個(gè)產(chǎn)品類的最大的實(shí)例化數(shù)量,可以通過判斷Map中已有對象的數(shù)量來實(shí)現(xiàn),這樣的處理是非常有意義的,例如JDBC連接數(shù)據(jù)庫,都會(huì)要求設(shè)置一個(gè)MaxConnections最大的連接數(shù)量,該數(shù)量就是內(nèi)存中最大實(shí)例化的數(shù)量。(優(yōu)秀的設(shè)計(jì)模式,到處都有體現(xiàn))
延遲加載還可以用在對象初始化比較復(fù)雜的情況下,例如硬件訪問,涉及多方面的交互,則可以通過延遲加載降低對象的產(chǎn)生和銷毀帶來的復(fù)雜性。
-
最佳實(shí)踐
工廠方法模式在項(xiàng)目中使用的非常頻繁,以至于很多代碼中都包含工廠模式。該模式幾乎盡人皆知,孰能生巧,熟練掌握該模式,多思考工廠方法如何應(yīng)用,而且工廠方法模式可以和其他模式混合使用(例如模板方法模式,單例模式,原型模式等),變化出無窮的優(yōu)秀設(shè)計(jì),這也正是軟件設(shè)計(jì)和開發(fā)的樂趣所在。(確實(shí),就學(xué)習(xí)這一模式都可以讓我想到很多平時(shí)常用的框架中的一些設(shè)計(jì)方式和基礎(chǔ)的思考方向,多學(xué)習(xí)總是可以觸類旁通的)
內(nèi)容來自《設(shè)計(jì)模式之禪》