設計模式詳解 簡單工廠模式

1.接口基礎回顧

設計模式六大原則,其中依賴倒轉原則,強調我們應該面向接口編程,那什么是接口?接口的作用? 接口如何使用?

1.1 接口回顧

1.接口的概念

接口是一種特殊的抽象類,跟一般的抽象類相比,接口里面的所有方法都是抽象方法,接口里面的所有屬性都是常量。也就是說,接口里面是只有方法定義而不會有任何方法實現(xiàn)。當然最新的jdk接口里面已經(jīng)可以有實現(xiàn)了, 當然那不是本章討論的重點.

2.接口的作用

通常用接口來定義實現(xiàn)類的外觀,也就是實現(xiàn)類的行為定義,用來約束實現(xiàn)類的行為。接口就相當于一份契約,根據(jù)外部應用需要的功能,約定了實現(xiàn)類應該要實現(xiàn)的功能,但是具體的實現(xiàn)類除了實現(xiàn)接口約定的功能外,還可以根據(jù)需要實現(xiàn)一些其它的功能,這是允許的,也就是說實現(xiàn)類的功能包含但不僅限于接口約束的功能

3.接口思想

根據(jù)接口的作用和用途,濃縮下來,接口的思想就是“封裝隔離”。
通常提到封裝是指對數(shù)據(jù)的封裝,但是這里的封裝是指“對被隔離體的行為的封裝”,或者是“對被隔離體的職責的封裝”;而隔離指的是外部調用和內部實現(xiàn),外部調用只能通過接口進行調用,而外部調用是不知道內部具體實現(xiàn)的,也就是說外部調用和內部實現(xiàn)是被接口隔離開的。

4.接口的好處

由于外部調用和內部實現(xiàn)被接口隔離開了,那么只要接口不變,內部實現(xiàn)的變化就不會影響到外部應用,從而使得系統(tǒng)更靈活,具有更好的擴展性和可維護性,這也就是所謂“接口是系統(tǒng)可插拔性的保證”這句話的意思。

5.接口和抽象類的選擇
既然接口是一種特殊的抽象類,那么在開發(fā)中,何時選用接口,何時選用抽象類呢?對于它們的選擇,在開發(fā)中是一個很重要的問題,特別總結兩句話給大家:優(yōu)先選用接口。在如下情況應選擇抽象類:既要定義子類的行為,又要為子類提供公共的功能。

1.2 面向接口編程

在Java 程序設計里面,非常講究層的劃分和模塊的劃分。通常按照三層來劃分Java程序,分別是表現(xiàn)層、邏輯層、數(shù)據(jù)層,它們之間都要通過接口來通訊。

在每一個層里面,又有很多個小模塊,一個小模塊對外也應該是一個整體,那么一個模塊對外也應該提供接口,其它地方需要使用到這個模塊的功能,都應該通過此接口來進行調用。這也就是常說的“接口是被其隔離部分的外觀”。基本的三層結構如圖所示:

三層結構示意圖

在一個層內部的各個模塊交互也要通過接口,如圖所示:


層內調用示意圖

上面頻頻提到“組件”,那么什么是組件呢?先簡單的名詞解釋一下:

組件從設計上講,組件就是能完成一定功能的封裝體,小到一個類,大到一個系統(tǒng),都可以稱為組件,因為一個小系統(tǒng)放到更大的系統(tǒng)里面去,也就當個組件而已。事實上,從設計的角度看,系統(tǒng)、子系統(tǒng)、模塊、組件等說的其實是同一回事情,都是完成一定功能的封裝體,只不過功能多少不同而已。

繼續(xù)剛才的思路,大家會發(fā)現(xiàn),不管是一層還是一個模塊或者一個組件,都是一個被接口隔離的整體,那么下面我們就不去區(qū)分它們,統(tǒng)一認為都是接口隔離體即可,如圖所示:

接口隔離體示意圖

1.3 不用設計模式是如何使用接口的

假設有一個接口叫Api,然后有一個實現(xiàn)類Impl實現(xiàn)了它,在客戶端怎么用這個接口呢?

接口和實現(xiàn)同客戶端的關系

通常都是在客戶端創(chuàng)建一個Impl的實例,把它賦值給一個Api接口類型的變量,然后客戶端就可以通過這個變量來操作接口的功能了

接口及實現(xiàn):

/**
 * 某個接口(通用的、抽象的、非具體的功能) 
 */
public interface Api {

    /**
     * 接口中的某個sayHello的功能
     */
    public void sayHello();
}

/**
 * 某個接口實現(xiàn)
 */
public class ApiImpl implements Api {

    @Override
    public void sayHello() {
        System.out.println("我是接口實現(xiàn)!");
    }
}

客戶端怎么使用接口, 按照Java的知識,接口不能直接使用,需要使用接口的實現(xiàn)類,示例代碼如下:

/** 
 *  客戶端:測試使用Api接口 
 */
public class Client {

    public static void main(String[] args) {
        Api impl = new ApiImpl();
        impl.sayHello();
    }
}

1.4 存在問題

你會發(fā)現(xiàn)在客戶端調用的時候,客戶端不但知道了接口,同時還知道了具體的實現(xiàn)就是ApiImpl。而接口的思想是“封裝隔離”,而Impl這個實現(xiàn)類,應該是被接口Api封裝并與客戶端隔離開的,也就是說,客戶端根本就不應該知道具體的實現(xiàn)類是Impl

我們要解決的問題就變成了:

只知道接口而不知實現(xiàn),該怎么獲得這個接口的實例?

就像現(xiàn)在的Client,它知道要使用Api接口,但是不知由誰實現(xiàn),也不知道如何實現(xiàn),從而得不到接口對象,就無法使用接口,該怎么辦呢?

2.解決方案

用來解決上述問題的一個合理的解決方案就是簡單工廠,那么什么是簡單工廠呢?,

1.概念

提供一個創(chuàng)建實例的功能,而無需關心具體的實現(xiàn)。被創(chuàng)建實例的類型可以是接口,抽象類,和普通類。

簡單工廠模式是類的創(chuàng)建模式,又叫做靜態(tài)工廠方法(Static Factory Method)模式。簡單工廠模式是由一個工廠對象決定創(chuàng)建出哪一種產(chǎn)品類的實例。

2.1 應用簡單工廠來解決的思路

分析上面的問題,雖然不能讓模塊外部知道模塊內的具體實現(xiàn),但是模塊內部是可以知道實現(xiàn)類的,而且創(chuàng)建接口是需要具體實現(xiàn)類的。那么干脆在模塊內部新建一個類,在這個類里面來創(chuàng)建接口,然后把創(chuàng)建好的接口返回給客戶端,這樣外部應用就只需要根據(jù)這個類來獲取相應的接口對象,然后就可以操作接口定義的方法了。把這樣的對象稱為簡單工廠,就叫Factory吧。
這樣一來,客戶端就可以通過這個Factory來獲取需要的接口對象,然后調用接口的方法來實現(xiàn)需要的功能,而且客戶端也不用再關心具體實現(xiàn)了。

2.2 簡單工廠結構

簡單工廠

Api:定義客戶所需要的功能接口

ApiImpl:具體實現(xiàn)Api的實現(xiàn)類,可能會有多個

Factory:工廠,選擇合適的實現(xiàn)類來創(chuàng)建Api接口對象

Client:客戶端,通過Factory去獲取Api接口對象,然后面向Api接口編程

2.3 通過簡單工廠重寫示意

Api 和 ApiImpl的定義同上一樣, 下面看看工廠類, 通常是靜態(tài)方法:

/**
 * 簡單工廠類
 */
public class Factory {

    /** 
     * 具體的創(chuàng)造Api對象的方法
     * @param type 示意,從外部傳入的選擇條件 
     * @return 創(chuàng)造好的Api對象 
     */
    
    public static Api createApi(int type) {
        switch (type) {
        case 1:
            return new ApiImpl();
        case 2:
            return new ApiImpl2();
        default:
            break;
        }
        return null;
    }
}

再來看看客戶端的示意,示例代碼如下:

/** 
 *  客戶端:測試使用Api接口 
 */
public class Client {

    public static void main(String[] args) {
        // 通常工廠類的方法是靜態(tài)的, 此處傳參數(shù)改變獲取到的具體實現(xiàn)
        Api impl = Factory.createApi(2);
        impl.sayHello();
    }
}

就如同上面的示例,客戶端通過簡單工廠創(chuàng)建了一個實現(xiàn)接口的對象,然后面向接口編程,從客戶端來看,它根本就不知道具體的實現(xiàn)是什么,也不知道是如何實現(xiàn)的,它只知道通過工廠獲得了一個接口對象,然后就能通過這個接口來獲取想要的功能。

事實上,簡單工廠能幫助我們真正開始面向接口編程,像以前的做法,其實只是用到了接口的多態(tài)那部分的功能,最重要的“封裝隔離性”并沒有體現(xiàn)出來。

3.模式講解

3.1 疑問

上面示例中的簡單工廠看起來不就是把客戶端里面的“new ApiImpl()”移動到簡單工廠里面嗎?不還是一樣通過new一個實現(xiàn)類來得到接口嗎?把“new ApiImpl()”這句話放到客戶端和放到簡單工廠里面有什么不同嗎?

理解這個問題的重點就在于理解簡單工廠所處的位置。

根據(jù)前面的學習,我們知道接口是用來封裝隔離具體的實現(xiàn)的,目標就是不要讓客戶端知道封裝體內部的具體實現(xiàn)簡單工廠的位置是位于封裝體內的,也就是簡單工廠是跟接口和具體的實現(xiàn)在一起的,算是封裝體內部的一個類,所以簡單工廠知道具體的實現(xiàn)類是沒有關系的

如圖所示, 對于客戶端, 虛線內部是它看不到的部分:


不可見內部

圖中虛線框,就好比是一個組件的包裝邊界,表示接口、實現(xiàn)類和工廠類組合成了一個組件,在這個封裝體里面,只有接口和工廠是對外的,也就是讓外部知道并使用的,所以故意漏了一些在虛線框外,而具體的實現(xiàn)類是不對外的,被完全包含在虛線框內。

對于客戶端而言,只是知道了接口Api和簡單工廠Factory,通過Factory就可以獲得Api了,這樣就達到了讓Client在不知道具體實現(xiàn)類的情況下獲取接口Api。所以看似簡單的把“new ApiImpl()”這句話從客戶端里面移動到了簡單工廠里面,其實是有了質的變化的。

3.2 認識簡單工廠

  • 簡單工廠的功能

工廠嘛,就是用來造東西的。在Java里面,通常情況下是用來造接口的,但是也可以造抽象類,比如本文下面有一個計算器的例子,甚至是一個具體的類實例。一定要注意,雖然前面的示例是利用簡單工廠來創(chuàng)建的接口,但是也是可以用簡單工廠來創(chuàng)建抽象類或者是普通類的實例的

  • 靜態(tài)工廠

使用簡單工廠的時候,通常不用創(chuàng)建簡單工廠類的類實例,沒有創(chuàng)建實例的必要。因此可以把簡單工廠類實現(xiàn)成一個工具類,直接使用靜態(tài)方法就可以了,也就是說簡單工廠的方法通常都是靜態(tài)的,所以也被稱為靜態(tài)工廠。如果要防止客戶端無謂的創(chuàng)造簡單工廠實例,還可以把簡單工廠的構造方法私有化了。

  • 萬能工廠

一個簡單工廠可以包含很多用來構造東西的方法,這些方法可以創(chuàng)造不同的接口、抽象類或者是類實例,一個簡單工廠理論上可以構造任何東西,所以又稱之為“萬能工廠”。雖然上面的實例中,在簡單工廠里面只有一個方法,但事實上,是可以有很多這樣創(chuàng)建方法的,這點要注意。

  • 簡單工廠創(chuàng)建對象的范圍
    雖然從理論上講,簡單工廠什么都能造,但對于簡單工廠可創(chuàng)建對象的范圍,通常不要太大,建議控制在一個獨立的組件級別或者一個模塊級別,也就是一個組件或模塊一個簡單工廠。否則這個簡單工廠類會職責不明,有點大雜燴的感覺。

3.3 簡單工廠中方法的寫法

雖然說簡單工廠的方法多是用來造接口的,但是仔細分析就會發(fā)現(xiàn),真正能實現(xiàn)功能的是具體的實現(xiàn)類,這些實現(xiàn)類是已經(jīng)做好的,并不是真的靠簡單工廠來創(chuàng)造出來的,簡單工廠的方法無外乎就是:實現(xiàn)了選擇一個合適的實現(xiàn)類來使用

所以簡單工廠方法的內部主要實現(xiàn)的功能是“選擇合適的實現(xiàn)類”來創(chuàng)建實例對象。既然要實現(xiàn)選擇,那么就需要選擇的條件或者是選擇的參數(shù),選擇條件或者是參數(shù)的來源通常又有幾種:

來源于客戶端,由Client來傳入?yún)?shù)

來源于配置文件,從配置文件獲取用于判斷的值

來源于程序運行期的某個值,比如從緩存中獲取某個運行期的值

由Client來傳入?yún)?shù)的簡單工廠:

/**
 * 簡單工廠類
 */
public class Factory {

    /** 
     * 具體的創(chuàng)造Api對象的方法
     * @param type 示意,從外部傳入的選擇條件 
     * @return 創(chuàng)造好的Api對象 
     */
    
    public static Api createApi(int type) {
        switch (type) {
        case 1:
            return new ApiImpl();
        case 2:
            return new ApiImpl2();
        default:
            break;
        }
        return null;
    }
}

要注意這種方法有一個缺點

由于是從客戶端在調用工廠的時候,傳入選擇的參數(shù),這就說明客戶端必須知道每個參數(shù)的含義,也需要理解每個參數(shù)對應的功能處理。這就要求必須在一定程度上,向客戶暴露一定的內部實現(xiàn)細節(jié)

3.4 可配置的簡單工廠

在現(xiàn)在的實現(xiàn)中,再新增加一種實現(xiàn),會怎樣呢?

那就需要修改工廠類,才能把新的實現(xiàn)添加到現(xiàn)有系統(tǒng)中。
每次新增加一個實現(xiàn)類都來修改工廠類的實現(xiàn),肯定不是一個好的實現(xiàn)方式。那么現(xiàn)在希望新增加了實現(xiàn)類過后不修改工廠類,該怎么辦呢?

一個解決的方法就是使用配置文件,當有了新的實現(xiàn)類過后,只要在配置文件里面配置上新的實現(xiàn)類就好了,在簡單工廠的方法里面可以使用反射,當然也可以使用IoC/DI(控制反轉/依賴注入,這個不在這里討論)來實現(xiàn)。

看看如何使用反射加上配置文件,來實現(xiàn)添加新的實現(xiàn)類過后,無須修改代碼,就能把這個新的實現(xiàn)類加入應用中。

配置文件用最簡單的properties文件,實際開發(fā)中多是xml配置。定義一個名稱為“FactoryTest.properties”的配置文件,放置到Factory同一個包下面,內容如下:

ImplClass=cn.javass.dp.simplefactory.example5.Impl

如果新添加了實現(xiàn)類,修改這里的配置就可以了,就不需要修改程序了。

/**
* 工廠類,用來創(chuàng)造Api對象
*/ 
public class Factory { 
    /**
    * 具體的創(chuàng)造Api的方法,根據(jù)配置文件的參數(shù)來創(chuàng)建接口
    * @return 創(chuàng)造好的Api對象
    */ 
    public static Api createApi(){ 
        //直接讀取配置文件來獲取需要創(chuàng)建實例的類 
        //至于如何讀取Properties,還有如何反射這里就不解釋了 
        Properties p = new Properties(); 
        InputStream in = null; 
        try { 
            in = Factory.class.getResourceAsStream("FactoryTest.properties"); 
            p.load(in); 
        } catch (IOException e) { 
            System.out.println("裝載工廠配置文件出錯了,具體的堆棧信息如下:"); 
            e.printStackTrace(); 
        }finally{ 
            try { 
                in.close(); 
            } catch (IOException e) { 
                e.printStackTrace(); 
            } 
        } 
        //用反射去創(chuàng)建,那些例外處理等完善的工作這里就不做了 
        Api api = null; 
        try { 
            api = (Api)Class.forName(p.getProperty("ImplClass")).newInstance(); 
        } catch (InstantiationException e) { 
            e.printStackTrace(); 
        } catch (IllegalAccessException e) { 
            e.printStackTrace(); 
        } catch (ClassNotFoundException e) { 
            e.printStackTrace(); 
        } 
        return api; 
    } 
}

此時的客戶端就變得很簡單了,不再需要傳入?yún)?shù),代碼示例如下:

/** 
 *  客戶端:測試使用Api接口 
 */
public class Client {

    public static void main(String[] args) {
        Api impl = Factory.createApi();
        impl.sayHello();
    }
}

3.5 簡單工廠的優(yōu)缺點

幫助封裝:簡單工廠雖然很簡單,但是非常友好的幫助我們實現(xiàn)了組件的封裝,然后讓組件外部能真正面向接口編程。

解耦:通過簡單工廠,實現(xiàn)了客戶端和具體實現(xiàn)類的解耦。如同上面的例子,客戶端根本就不知道具體是由誰來實現(xiàn),也不知道具體是如何實現(xiàn)的,客戶端只是通過工廠獲取它需要的接口對象。

可能增加客戶端的復雜度:如果通過客戶端的參數(shù)來選擇具體的實現(xiàn)類,那么就必須讓客戶端能理解各個參數(shù)所代表的具體功能和含義,這會增加客戶端使用的難度,也部分暴露了內部實現(xiàn),這種情況可以選用可配置的方式來實現(xiàn)。

不方便擴展子工廠:私有化簡單工廠的構造方法,使用靜態(tài)方法來創(chuàng)建接口,也就不能通過寫簡單工廠類的子類來改變創(chuàng)建接口的方法的行為了。不過,通常情況下是不需要為簡單工廠創(chuàng)建子類的。

3.6 思考簡單工廠

  • 簡單工廠的本質 : 選擇實現(xiàn)

注意簡單工廠的重點在選擇,實現(xiàn)是已經(jīng)做好了的。就算實現(xiàn)再簡單,也要由具體的實現(xiàn)類來實現(xiàn),而不是在簡單工廠里面來實現(xiàn)。簡單工廠的目的在于為客戶端來選擇相應的實現(xiàn),從而使得客戶端和實現(xiàn)之間解耦,這樣一來,具體實現(xiàn)發(fā)生了變化,就不用變動客戶端了,這個變化會被簡單工廠吸收和屏蔽掉

實現(xiàn)簡單工廠的難點就在于“如何選擇”實現(xiàn),前面講到了幾種傳遞參數(shù)的方法,那都是靜態(tài)的參數(shù),還可以實現(xiàn)成為動態(tài)的參數(shù)。比如:在運行期間,由工廠去讀取某個內存的值,或者是去讀取數(shù)據(jù)庫中的值,然后根據(jù)這個值來選擇具體的實現(xiàn)等等。

  • 何時選用簡單工廠

建議在如下情況中,選用簡單工廠:

  1. 如果想要完全封裝隔離具體實現(xiàn),讓外部只能通過接口來操作封裝體,那么可以選用簡單工廠,讓客戶端通過工廠來獲取相應的接口,而無需關心具體實現(xiàn);
  1. 如果想要把對外創(chuàng)建對象的職責集中管理和控制,可以選用簡單工廠,一個簡單工廠可以創(chuàng)建很多的、不相關的對象,可以把對外創(chuàng)建對象的職責集中到一個簡單工廠來,從而實現(xiàn)集中管理和控制。

3.7 相關模式

  • 簡單工廠和抽象工廠模式

簡單工廠是用來選擇實現(xiàn)的,可以選擇任意接口的實現(xiàn),一個簡單工廠可以有多個用于選擇并創(chuàng)建對象的方法,多個方法創(chuàng)建的對象可以有關系也可以沒有關系。

抽象工廠模式是用來選擇產(chǎn)品簇的實現(xiàn)的,也就是說一般抽象工廠里面有多個用于選擇并創(chuàng)建對象的方法,但是這些方法所創(chuàng)建的對象之間通常是有關系的,這些被創(chuàng)建的對象通常是構成一個產(chǎn)品簇所需要的部件對象。

所以從某種意義上來說,簡單工廠和抽象工廠是類似的,如果抽象工廠退化成為只有一個實現(xiàn),不分層次,那么就相當于簡單工廠了。

  • 簡單工廠和工廠方法模式

簡單工廠和工廠方法模式也是非常類似的。工廠方法的本質也是用來選擇實現(xiàn)的,跟簡單工廠的區(qū)別在于工廠方法是把選擇具體實現(xiàn)的功能延遲到子類去實現(xiàn)。如果把工廠方法中選擇的實現(xiàn)放到父類直接實現(xiàn),那就等同于簡單工廠。

  • 簡單工廠和能創(chuàng)建對象實例的模式

簡單工廠的本質是選擇實現(xiàn),所以它可以跟其它任何能夠具體的創(chuàng)建對象實例的模式配合使用,比如:單例模式、原型模式、生成器模式等等。


4.最簡單的例子:計算器的小程序(利用抽象類)

4.1 求兩個數(shù)之和

public class App {
    public static void main(String[] args) {
    
        Scanner sc = new Scanner(System.in);
        System.out.println("請輸入數(shù)字A后按回車鍵");
        double numberA = sc.nextDouble();
        System.out.println("請輸入數(shù)字B后按回車鍵");
        double numberB = sc.nextDouble();
        System.out.println("計算結果: " + (numberA + numberB));
        sc.close();
    }
}

現(xiàn)在需求變更, 求兩數(shù)之差

改代碼? 把加號改成減號?是結果是有了, 再改變需求呢?乘法除法, 再改..
鑒于需求不斷改變, 封裝一個計算方法在這個App類中, 便于復用.

改良后代碼:

public class App {
    public static void main(String[] args) {
    
        Scanner sc = new Scanner(System.in);
        System.out.println("請輸入數(shù)字A后按回車鍵");
        double numberA = sc.nextDouble();
        System.out.println("請輸入數(shù)字B后按回車鍵");
        double numberB = sc.nextDouble();
        System.out.println("請輸入符號后按回車鍵");
        String sign = sc.next();
        System.out.println("計算結果: " + operating(numberA,numberB,sign));
        sc.close();
    }
    // 計算方法
    public static double operating(double numberA, double numberB, String sign) {
        if ("+".equals(opreate)) {
            return numberA + numberB;
        } else if ("-".equals(opreate)) {
            return numberA - numberB;
        } else if ("*".equals(opreate)) {
            return numberA * numberB;
        } else if ("/".equals(opreate)) {
            return numberA / numberB;
        }
        return 0;
    }
}

記得當初學習編程的時候,自己練習過這種計算小程序,也是寫出這種代碼,當時覺得自己牛逼的可以了,還會封裝呢,哈哈..

4.2 把業(yè)務和界面分開

思考:如果現(xiàn)在不從控制臺輸入數(shù)字進行計算了, 改為從網(wǎng)頁輸入數(shù)字了,那這個app類也用不上了,難道復制一套計算的方法么? 所以實際上, 應該先把輸入數(shù)字的界面和計算的業(yè)務首先分開

例如這樣:

public class Operate {

    public static double operating(double a, double b, String sign) {
        if ("+".equals(sign)) {
            return numberA + numberB;
        } else if ("-".equals(sign)) {
            return numberA - numberB;
        } else if ("*".equals(sign)) {
            return numberA * numberB;
        } else if ("/".equals(sign)) {
            return numberA / numberB;
        }
        return 0;
    }
}

//App類調用: 
Operate.operating(numberA,numberB,sign);

這樣的話不論你什么方式計算, 只要在相應的類中調用Operate.operating(numberA,numberB,sign);這段代碼就可以了.

4.3 新的問題,兩數(shù)相除得驗證除數(shù)是否為0啊

最差解決方案: 在除法的if判斷里加層邏輯判斷么?判斷b的值是否為0? 可以解決, 但是不是最好的方案. 但違背軟件設計的開閉原則

如果此時再來個需求, 計算兩個數(shù)的差再求和, 在Operate類中增加switch的分支么? 可以解決, 不過同上道理, 代碼會越來越臃腫, 并且違背開閉原則

其實就這個模擬計算的程序而言, 怎么寫可能都難以看出區(qū)別, 但是對于真正項目來說, 代碼的優(yōu)美, 對于程序的穩(wěn)定和維護實際意義要大的多.

那么針對上述幾個無聊的需求來說, 首先“除數(shù)為0”是只針對除法對象的需求, 利用面向對象的思想, "加法"對象會說, 你"0"不"0"的關老子鳥事? 對, 不關其他符號對象的鳥事!自己解決自己的事嘛!

說白了, 就是分離加 減 乘 除 他們, 讓他們作為對象存在, 不管自己的算法有多復雜, 或者邏輯有多復雜, 都跟其他符號沒關系, 減少代碼維護的風險和難度.

另外, 大話設計模式這本書, 說的需求例子也很好, 我不細致描述了, 意思差不多, 有書的朋友可以參考.

所以我們需要分離運算符

將 加 減 乘 除 分離開, 四個對象均繼承自運算Operate對象

Operate抽象類 具有計兩個數(shù)的方法, 具體實現(xiàn)由 他們自己完成

public abstract class Operate {
    public abstract double operating(double a, double b);
}

加法, 除法的代碼,其他算法略, 一個意思都

public class OperateAdd extends Operate {
    public double operating(double a, double b) {
        return a + b;
    }
}
public class OperateDiv extends Operate {
    public double operating(double a, double b) {
        if (b == 0) {
            throw 異常("除數(shù)為0....");
        }
        double result = a / b;
        if (result == 0) {
            控制臺提示("結果為0")....
        }
        return result;
    }
}

思考:現(xiàn)在問題是什么? 我在調用時怎么知道客戶端需要的是哪個運算?

這就跟開篇1.4章節(jié)講的問題是一樣的了, 如下:

只知道Operate抽象類,而不知道具體實現(xiàn),該怎么獲得這個子類的實例?

解決方案

這時就需要 "簡單工廠模式", 用一個單獨的類, 來做這個創(chuàng)造實例的過程.

那么工廠要想知道我們需要哪個運算類, 客戶端需要告訴它符號是什么對吧?

因為面向對象的思想, 甚至符號我們也可以當成對象, 在里面寫一些判斷輸入的符號是否合法之類的代碼

符號Sign對象代碼:

public class SignObj {
    private String sign;

    public String getSign() {
        return sign;
    }

    public void setSign(String sign) {
        if (sign == null || sign == '') {
            拋異常...("輸入符號非法")
        }
        this.sign = sign;
    }
}

簡單工廠的創(chuàng)建實例方法的代碼:

public static Operate creteOperate(SignObj obj) {
    if (obj == null || obj.getSign() == null) {
        return null;
    }
    Operate opet = null;
    switch (obj.getSign()) {
    case "+":
        opet = new OperateAdd();
        break;
    case "-":
        ...
        break;
    case "*":
        ...
        break;
    case "/":
        opet = new OperateDiv();
        break;  
    default:
        break;
    }
    return opet;
}

最后app調用:

// 改良后
SignObj signObj = new SignObj();
signObj.setSign("/");
Operate opet = OperateFactory.creteOperate(signObj);
System.out.println("計算結果: " + opet.getResult(numberA, numberB));

附上UML類圖(圖畫的不全, 只畫了加法和減法, 除法和乘法和前面兩者一樣的,沒畫)


image

5.實際web應用的例子

各位基本上都使用過或者將來要使用的一個例子來說明簡單工廠模式,我們去模擬一個簡單的struts2的功能。

本例會制作一個簡單的WEB項目,其中會忽略掉很多細節(jié),目的是為了突出我們的簡單工廠模式。

眾所周知,我們平時開發(fā)web項目大部分是以spring作為平臺,來集成各個組件,比如集成struts2來完成業(yè)務層與表現(xiàn)層的邏輯,集成hibernate或者ibatis來完成持久層的邏輯。

struts2在這個過程當中提供了分離數(shù)據(jù)持久層,業(yè)務邏輯層以及表現(xiàn)層的責任,有了Struts2,我們不再需要servlet,而是可以將一個普通的Action類作為處理業(yè)務邏輯的單元,然后將表現(xiàn)層交給特定的視圖去處理,比如JSP,template等等。

我們來嘗試著寫一個非常非常簡單的web項目,來看看在最原始的時候,也就是沒有spring,struts2等等這些個開源框架的時候,我們都是怎么過的。

5.1 web項目的類

假設這是一個小型的WEB項目,我們通常里面會有這些類:


//數(shù)據(jù)源連接池,用來生產(chǎn)數(shù)據(jù)庫連接。
class DataSource{}

//我們一般會有這樣一個數(shù)據(jù)訪問的基類,這個類要依賴于數(shù)據(jù)源
class BaseDao{}
    
//一般會有一系列這樣的DAO去繼承BaseDao,這一系列的DAO類便是數(shù)據(jù)持久層
class UserDao extends BaseDao{}
class PersonDao extends BaseDao{}
class EmployeeDao extends BaseDao{}
    
//我們還會有一系列這樣的servlet,他們通常依賴于各個Dao類,這一系列servlet便是我們的業(yè)務層
class LoginServlet extends HttpServlet{}
class LoginOutServlet extends HttpServlet{}
class RegisterServlet extends HttpServlet{}
    
//我們通常還會有HTML頁面或者JSP頁面,但是這個本次不在考慮范圍內,這便是表示層。

以上是我們小型WEB項目大體的結構,可以看到三個Servlet沒有寫具體的實現(xiàn)到底如何,但是不難猜測,三個Servlet的功能分別是進行登錄,注銷,以及注冊新用戶的功能。

我們的servlet一般都是繼承自HttpServlet,因為我們在web.xml配置servlet時,所寫入的Class需要實現(xiàn)Servlet接口,而我們通常采用的傳輸協(xié)議都是HTTP,所以HttpServlet就是我們最好的選擇了,它幫我們完成了基本的實現(xiàn)。

5.2 存在的問題

但是這樣我們有很多限制,比如我們一個Servlet一般只能負責一個單一的業(yè)務邏輯,因為我們所有的業(yè)務邏輯通常情況下都集中在doPost這樣一個方法當中,可以想象下隨著業(yè)務的增加,我們的Servlet數(shù)量會高速增加,這樣不僅項目的類會繼續(xù)增加,最最惡心的是,我們每添加一個Servlet就要在web.xml里面寫一個servlet配置。

但是如果我們讓一個Servlet負責多種業(yè)務邏輯的話,那我們需要在doPost方法中加入很多if判斷,去判斷當前的操作。

比如我們將上述三個servlet合一的話,你會在doPost出現(xiàn)以下代碼。

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //我們加入一個操作的參數(shù),來讓servlet做出不同的業(yè)務處理
        String operation = req.getParameter("operation");
        if (operation.equals("login")) {
            System.out.println("login");
        }else if (operation.equals("register")) {
            System.out.println("register");
        }else if (operation.equals("loginout")) {
            System.out.println("loginout");
        }else {
            throw new RuntimeException("invalid operation");
        }
    }

或者再好一點,會這么寫

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //我們加入一個操作的參數(shù),來讓servlet做出不同的業(yè)務處理
        String operation = req.getParameter("operation");
        if (operation.equals("login")) {
            login();
        }else if (operation.equals("register")) {
            register();
        }else if (operation.equals("loginout")) {
            loginout();
        }else {
            throw new RuntimeException("invalid operation");
        }
    }
    
    private void login(){
        System.out.println("login");
    }
    
    private void register(){
        System.out.println("register");
    }
    
    private void loginout(){
        System.out.println("loginout");
    }

雖然我們已經(jīng)將各個單一的業(yè)務邏輯拆分成方法,但這依然是違背單一原則的,因為我們的servlet應該只是處理業(yè)務邏輯,而不應該還要負責與業(yè)務邏輯不相關的處理方法定位這樣的責任,這個責任應該交給請求方,原本在三個servlet分別處理登陸,注銷和注冊的時候,其實就是這樣的,作為請求方,只要是請求LoginServlet,就說明請求的人是要登陸,處理這個請求的servlet不需要再出現(xiàn)有關判斷請求操作的代碼。

所以我們需要想辦法把判斷的業(yè)務邏輯交給請求方去處理,回想下struts2的做法,我們來模擬一個分配請求的過濾器,它的任務就是根據(jù)用戶的請求去產(chǎn)生響應的servlet處理請求

我們用這個過濾器來消除servlet在web.xml的配置,幫我們加快開發(fā)的速度,我們寫出如下filter。

5.3 增加過濾器

/用來分派請求的filter
public class DispatcherFilter implements Filter{
    
    private static final String URL_SEPARATOR = "/";
    
    private static final String SERVLET_PREFIX = "servlet/";
    
    private String servletName;
    
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterChain) throws IOException, ServletException {
        parseRequestURI((HttpServletRequest) servletRequest);
        //這里為了體現(xiàn)我們本節(jié)的重點,我們采用一個工廠來幫我們制造Action
        if (servletName != null) {
            //這里使用的正是簡單工廠模式,創(chuàng)造出一個servlet,然后我們將請求轉交給servlet處理
            Servlet servlet = ServletFactory.createServlet(servletName);
            servlet.service(servletRequest, servletResponse);
        }else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    
    //負責解析請求的URI,我們約定請求的格式必須是/contextPath/servlet/servletName
    private void parseRequestURI(HttpServletRequest httpServletRequest){
        String validURI = httpServletRequest.getRequestURI().replaceFirst(httpServletRequest.getContextPath() + URL_SEPARATOR, "");
        if (validURI.startsWith(SERVLET_PREFIX)) {
            servletName = validURI.split(URL_SEPARATOR)[1];
        }
    }

}

這個filter需要在web.xml中加入以下配置

<filter>
  <filter-name>dispatcherFilter</filter-name>
  <filter-class>com.web.filter.DispatcherFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>dispatcherFilter</filter-name>
  <url-pattern>/servlet/*</url-pattern>
</filter-mapping>

下面給出我們的主角,我們的servlet工廠,它就相當于上面的Factory。

public class ServletFactory {

    private ServletFactory(){}
    //一個servlet工廠,專門用來生產(chǎn)各個servlet,而我們生產(chǎn)的依據(jù)就是傳入的servletName,
    //這個serlvetName由我們在filter截獲,傳給servlet工廠。
    public static Servlet createServlet(String servletName){
        if (servletName.equals("login")) {
            return new LoginServlet();
        }else if (servletName.equals("register")) {
            return new RegisterServlet();
        }else if (servletName.equals("loginout")) {
            return new LoginoutServlet();
        }else {
            throw new ServletException("unknown servlet");
        }
    }
}

看到這里,是不是有點感覺了呢,我們一步一步去消除servlet的XML配置的過程,其實就是在慢慢的寫出一個簡單工廠模式,只是在這之中,抽象的產(chǎn)品接口是現(xiàn)成的,也就是Servlet接口。

雖說這些個elseif并不是好代碼的征兆,不過這個簡單工廠最起碼幫我們解決了惡心的xml配置,說起來也算功不可沒。

簡單工廠是設計模式當中相對比較簡單的模式,它甚至都沒資格進入GOF的二十三種設計模式,所以可見它多么卑微了,但就是這么卑微的一個設計模式,也能真正的幫我們解決實際當中的問題,雖說這種解決一般只能針對規(guī)模較小的項目。


參考文檔


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,766評論 18 399
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,948評論 18 139
  • 從三月份找實習到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,367評論 11 349
  • 1 場景問題# 大家都知道,在Java應用開發(fā)中,要“面向接口編程”。那么什么是接口?接口有什么作用?接口如何使用...
    七寸知架構閱讀 6,509評論 14 70
  • 今日一大早,驅車趕往安徽六安市。 事先查了一下高德地圖,想像著自己在高速路上的長途奔襲,心里有些打怵,過一會又安慰...
    昆侖濯羽閱讀 189評論 4 2