23種設(shè)計模式-工廠方法模式

  1. 女媧造人的故事

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

8-1

類圖比較簡單,AbstractHumanFactory是一個抽象類,定義了一個八卦爐具有的整體功能,HumanFactory為實現(xiàn)類,完成具體的任務(wù)——創(chuàng)建人類;Human接口是人類的總稱,其三個實現(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();
    }
}

以上就是工廠模式。

  1. 工廠模式定義

工廠模式使用的頻率非常高,在我們?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(定義一個工廠用于創(chuàng)建對象的接口,讓子類決定實例化哪一個類。工廠方法是一個類的實例化延遲到其子類)

工廠方法模式的通用類圖8-2所示:


8-2

在工廠方法模式中,抽象產(chǎn)品Product負責定義產(chǎn)品的共性,實現(xiàn)對事物最抽象的定義:Creator 為抽象創(chuàng)建類,也就是抽象工廠,具體如何創(chuàng)建產(chǎn)品類是由具體的實現(xiàn)工廠ConcreteCreator完成的。工廠方法模式的變種較多,我們來看一個比較實用的通用源碼。

//抽象產(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);
}
//工廠實現(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();
    }
}
  1. 工廠方法模式的應(yīng)用

3.1 工廠方法模式的優(yōu)點
首先,良好的封裝性,代碼結(jié)構(gòu)清晰。一個對象創(chuàng)建是有條件約束的,如一個調(diào)用者需要一個具體的產(chǎn)品對象,只要知道這個產(chǎn)品的類名就可以了,不用知道創(chuàng)建對象的艱辛過程,降低模塊間的耦合。
其次,工廠方法模塊的擴展性非常優(yōu)秀。在增加產(chǎn)品類的情況下,只要適當?shù)男薷木唧w的工廠類或擴展一個工廠實現(xiàn)類,就可以完成“擁抱變化”。例如在我們的例子中,需要增加一個棕色人種,只需要增加一個brownHuman類,工廠類不用任何修改就可以完成系統(tǒng)擴展。
再次,屏蔽產(chǎn)品類。這一特點非常重要,產(chǎn)品類的實現(xiàn)如何變化,調(diào)用者都不需要關(guān)心,他只需要關(guān)心產(chǎn)品的接口,只要接口保持不變,系統(tǒng)中的上層模塊就不要發(fā)生變化。因為產(chǎn)品類的實例工作是由工廠負責的,一個產(chǎn)品對象具體由哪一個產(chǎn)品生成是由工廠類決定的。在數(shù)據(jù)庫開發(fā)中,大家應(yīng)該能夠深刻的體會到工廠方法模式的好處:如果使用JDBC連接數(shù)據(jù)庫,數(shù)據(jù)庫從Mysql切換到Oracle,需要改動的地方就是切換一下驅(qū)動名稱(前提條件是sql語句是標準語句),其他的都不需要修改,這是工廠模式靈活性的一個直接案例。
最后,工廠方法模式是典型的解耦框架。高層模塊值需要知道產(chǎn)品的抽象類,其他的實現(xiàn)類都不用關(guān)心,符合迪米特法則(最少知識原則),我不需要的就不去交流;也符合依賴倒置原則,只依賴產(chǎn)品類的抽象;當然也符合里氏替換原則,使用產(chǎn)品子類替換產(chǎn)品父類,沒問題!(六大原則指導(dǎo)思想,設(shè)計模式是六大原則思想的具體實現(xiàn))

3.2 工廠方法模式的使用場景
首先,工廠方法模式是new一個對象的替代品,所以在所有需要生成對象的地方都可以使用,但是需要慎重考慮是否要增加一個工廠類進行管理,增加代碼的復(fù)雜度。
其次,需要靈活的、可擴展的框架時,可以考慮采用工廠模式。萬物皆對象,那萬物也就皆產(chǎn)品類,例如需要設(shè)計一個郵件服務(wù)器的框架,有三種網(wǎng)絡(luò)協(xié)議可供選擇:POP3、IMAP、HTTP,我們就可以把這三中連接方式作為產(chǎn)品類,定義一個接口如IConnectMail,然后定義對郵件的操作方法,用不同的方法是想三個具體的產(chǎn)品類(也就是連接方式)再定義一個工廠類,按照不同的傳入條件,選擇不同的連接方式。如此設(shè)計,可以做到完美的擴展,如某些郵件服務(wù)器提供webservice接口,我們只需要增加一個產(chǎn)品類就可以了。
再次,工廠方法模式可以用在異構(gòu)項目中,例如通過webservice與一個非java的項目交付,雖然webservice號稱可以做到異構(gòu)系統(tǒng)的同構(gòu)化,但是在實踐的開發(fā)中,還是會碰到很多問題,如類型問題、WSDL文件的支持問題,等等。從WSDL中產(chǎn)生的對象都認為是一個產(chǎn)品,然后由一個具體的工廠類進行管理,減少與外圍系統(tǒng)的耦合。(webservice的接口做得不多,這個意思是通過定義一個標識接口,讓外圍系統(tǒng)產(chǎn)生的對象都實現(xiàn)這個標識接口去統(tǒng)一管理嗎

  1. 工廠方法模式的擴展

工廠方法模式有很多擴展,而且與其他模式結(jié)合使用威力更大,下面將介紹4種擴展。(就我自己理解的一種就是可以把工廠類做成單例的,一般工廠會涉及很多配置信息,這個類就比較重,做成單例的就可以減少創(chuàng)建銷毀的開銷
4.1 縮小為簡單工廠模式
我們這樣考慮一個問題:一個模塊僅需要一個工廠類,沒必要把他產(chǎn)生出來,使用靜態(tài)的方法就好了,根據(jù)這一要求,我們把上例中的AbstractHumanFactory修改一下,類圖8-3:

8-3

在類圖中可以看到,去掉了抽象工廠類,同時把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;
    }
}

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

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

8-5

Singleton定義了一個private的無參構(gòu)造函數(shù),目的是不允許通過new的方式創(chuàng)建一個對象,代碼如下:

//單例類
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)限,生成一個對象,然后提供外部訪問,保證內(nèi)存中的對象唯一。當然其他類也可以通過反射的方式建立一個單例對象,確實如此,但是一個項目或團隊是由章程和規(guī)范的,何況已經(jīng)提供了一個獲得單例對象的方法,為什么還要重新創(chuàng)建一個新對象呢。(按這個方式去思考,我們常說spirng中默認bean是單例的,其實并不是真的單例類,你也可以自己手動去創(chuàng)建,但是我們一般不會這么做,那么在bean的配置中可以通過設(shè)置去創(chuàng)建單例或是多例,實際上也是這樣的邏輯去做的,通過beanFactory去管理的bean,這塊之后可以去通過看spring框架的源碼去學習一下。以上都是本人通過學習設(shè)計模式想到的一些,不知道是否正確,但是提供了一種思考思路??梢韵胍姡駍pring這些優(yōu)秀的框架,其中的設(shè)計模式的實際運用隨處可見,學完設(shè)計模式可以帶著這些設(shè)計理念去閱讀spring源碼,一定會非常有幫助
4.4 延遲初始化
何為延遲初始化(Lazy initialization)?一個對象被消費完畢后,并不立刻釋放,工廠類保持其初始狀態(tài),等待再次被使用。延遲初始化是工廠方法模式的一個擴展應(yīng)用(聽名字,應(yīng)該是一個典型的空間換時間的思路,對象消費完畢并不是立刻釋放,而是持有一段時間,下次在需要就直接使用,省去了創(chuàng)建銷毀的開銷,但是持有時會占用內(nèi)存),其通用類圖8-6:

8-6

ProductFactory負責產(chǎn)品類對象的創(chuàng)建工作,并且通過prMap變量產(chǎn)生一個緩存,對需要再次被重用的對象保留(看完類圖發(fā)現(xiàn),這個好像就是最基礎(chǔ)的緩存原型吧,用map將最近使用的對象存起來,取的時候先去map中找,找不到在去創(chuàng)建,然后將新創(chuàng)建的實例維護到map中,如果map中已經(jīng)存滿(一般緩存肯定會有一個上限,要不然內(nèi)存很容易就不夠用了,動不動就內(nèi)存溢出,那誰頂?shù)牧?,?yīng)用肯定會出問題),就將比較久遠的對象移除,在將新的對象保存到map中(這就是一個緩存策略的問題了)),代碼如下:

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)建其他實現(xiàn)類對應(yīng)的實例,就不寫了
           }
            prMap.put(type,product);
        }
        return product;
    }
}

延遲加載框架是可以擴展的,例如限制某一個產(chǎn)品類的最大的實例化數(shù)量,可以通過判斷Map中已有對象的數(shù)量來實現(xiàn),這樣的處理是非常有意義的,例如JDBC連接數(shù)據(jù)庫,都會要求設(shè)置一個MaxConnections最大的連接數(shù)量,該數(shù)量就是內(nèi)存中最大實例化的數(shù)量。(優(yōu)秀的設(shè)計模式,到處都有體現(xiàn)
延遲加載還可以用在對象初始化比較復(fù)雜的情況下,例如硬件訪問,涉及多方面的交互,則可以通過延遲加載降低對象的產(chǎn)生和銷毀帶來的復(fù)雜性。

  1. 最佳實踐

工廠方法模式在項目中使用的非常頻繁,以至于很多代碼中都包含工廠模式。該模式幾乎盡人皆知,孰能生巧,熟練掌握該模式,多思考工廠方法如何應(yīng)用,而且工廠方法模式可以和其他模式混合使用(例如模板方法模式,單例模式,原型模式等),變化出無窮的優(yōu)秀設(shè)計,這也正是軟件設(shè)計和開發(fā)的樂趣所在。(確實,就學習這一模式都可以讓我想到很多平時常用的框架中的一些設(shè)計方式和基礎(chǔ)的思考方向,多學習總是可以觸類旁通的

內(nèi)容來自《設(shè)計模式之禪》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容