Java設計模式解析---工廠方法模式

  • 所牽涉源代碼地址
    https://github.com/Wasabi1234/design-patterns

0 簡單工廠案例






JDK 應用實例

日曆類



迭代器

Collection 接口就相當於 VideoFactory

相當於各種具體的工廠,如 JavaVideoFactory
父類接口,子類實現
Itr 就是具體產品 JavaVideo

工廠應用

爲解決 url 協議擴展使用


Launcher#Factory靜態類

logback 應用

LoggerFactory#getLogger(String name)

JDBC實例

直接註冊 MySQL 驅動



返回值是一個抽象類,必有一子類實現其,看一下


通過間接繼承此處理器


這其中URLStreamHandler就相當於各種抽象產品,而其實現類即各種具體的產品
URLStreamHandlerFactory就相當於 VideoFactory
而如下 Factory 就相當於如 JavaVideoFactory/PythonVideoFactory

Logback實例



1 工廠方法模式案例

1.1 簡單工廠的升級




對造人過程進行分析,該過程涉及三個對象:女媧、八卦爐、三種不同膚色的人

  • 女媧可以使用場景類Client表示
  • 八卦爐類似於一個工廠,負責製造生產產品(即人類)
  • 三種不同膚色的人,他們都是同一個接口下的不同實現類,對於八卦爐來說都是它生產出的產品
    女媧造人類圖
  • 接口Human是對人類的總稱,每個人種都至少具有兩個方法
  • 黑色人種
  • 黃色人種
  • 白色人種

所有人種定義完畢,下一步就是定義一個八卦爐,然後燒製人類

最可能給八卦爐下達什麼樣的生產命令呢?
應該是給我生產出一個黃色人種(YellowHuman類)
而不會是給我生產一個會走、會跑、會說話、皮膚是黃色的人種
因爲這樣的命令增加了交流的成本,作爲一個生產的管理者,只要知道生產什麼就可以了,而不需要事物的具體信息

在這裏採用了泛型,通過定義泛型對createHuman的輸入參數產生兩層限制
● 必須是Class類型
● 必須是Human的實現類
其中的T表示,只要實現了Human接口的類都可以作爲參數

只有一個八卦爐,其實現生產人類的方法

人種有了,八卦爐也有了,剩下的工作就是女媧採集黃土,然後命令八卦爐開始生產

人種有了,八卦爐有了,負責生產的女媧也有了
運行一下,結果如下所示

案例 2

註冊工廠

  • Pet 層次生成對象的問題
    每當添加一種新Pet 類型,必須記住將其添加到 LiteralPetCreator.java 的條目中。在一個定期添加更多類的系統中,這可能會成爲問題。

你可能會考慮向每個子類添加靜態初始值設定項,因此初始值設定項會將其類添加到某個列表中。但靜態初始值設定項僅在首次加載類時調用:生成器的列表中沒有類,因此它無法創建該類的對象,因此類不會被加載並放入列表中。

必須自己手工創建列表。所以最好就是把列表集中放在一個明顯的地方:層次結構的基類

使用工廠方法設計模式將對象的創建推遲到類本身。
工廠方法以多態方式調用,創建適當類型的對象。
java.util.function.SupplierT get() 描述了原型工廠方法。協變返回類型允許 get()Supplier 的每個子類實現返回不同的類型。

在本例中,基類 Part 包含一個工廠對象的靜態列表,列表成員類型爲 Supplier<Part>。對於應該由 get() 方法生成的類型的工廠,通過將它們添加到 prototypes 列表向基類“註冊”。奇怪的是,這些工廠本身就是對象的實例。此列表中的每個對象都是用於創建其他對象的原型

// typeinfo/RegisteredFactories.java
// 註冊工廠到基礎類
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

class Part implements Supplier<Part> {
    @Override
    public String toString() {
        return getClass().getSimpleName();
    }

    static List<Supplier<? extends Part>> prototypes =
        Arrays.asList(
          new FuelFilter(),
          new AirFilter(),
          new CabinAirFilter(),
          new OilFilter(),
          new FanBelt(),
          new PowerSteeringBelt(),
          new GeneratorBelt()
        );

    private static Random rand = new Random(47);
    public Part get() {
        int n = rand.nextInt(prototypes.size());
        return prototypes.get(n).get();
    }
}

class Filter extends Part {}

class FuelFilter extends Filter {
    @Override
    public FuelFilter get() {
        return new FuelFilter();
    }
}

class AirFilter extends Filter {
    @Override
    public AirFilter get() {
        return new AirFilter();
    }
}

class CabinAirFilter extends Filter {
    @Override
    public CabinAirFilter get() {
        return new CabinAirFilter();
    }
}

class OilFilter extends Filter {
    @Override
    public OilFilter get() {
        return new OilFilter();
    }
}

class Belt extends Part {}

class FanBelt extends Belt {
    @Override
    public FanBelt get() {
        return new FanBelt();
    }
}

class GeneratorBelt extends Belt {
    @Override
    public GeneratorBelt get() {
        return new GeneratorBelt();
    }
}

class PowerSteeringBelt extends Belt {
    @Override
    public PowerSteeringBelt get() {
        return new PowerSteeringBelt();
    }
}

public class RegisteredFactories {
    public static void main(String[] args) {
        Stream.generate(new Part())
              .limit(10)
              .forEach(System.out::println);
    }
}

並非層次結構中的所有類都應實例化;這裏的 FilterBelt 只是分類器,這樣你就不會創建任何一個類的實例,而是隻創建它們的子類(請注意,如果嘗試這樣做,你將獲得 Part 基類的行爲)。

因爲 Part implements Supplier<Part>Part 通過其 get() 方法供應其他 Part。如果爲基類 Part 調用 get()(或者如果 generate() 調用 get()),它將創建隨機特定的 Part 子類型,每個子類型最終都從 Part 繼承,並重寫相應的 get() 以生成它們中的一個。

2 定義

  • 官方定義
    Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses
    定義一個用於創建對象的接口,讓子類決定實例化哪一個類。工廠方法使一個類的實例化延遲到其子類

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

  • 抽象產品類

具體的產品類可以有多個,都繼承於抽象產品類

  • 具體產品類

  • 抽象工廠類
    負責定義產品對象的產生

  • 具體工廠類
    具體如何產生一個產品的對象,是由具體的工廠類實現的

  • 場景類

該通用代碼是一個比較實用、易擴展的框架,讀者可以根據實際項目需要進行擴展

3 應用

3.1 優點


  • 良好的封裝性,代碼結構清晰
    一個對象創建是有條件約束的,如一個調用者需要一個具體的產品對象,只要知道這個產品的類名(或約束字符串)就可以了,不用知道創建對象的艱辛過程,降低模塊間的耦合
  • 工廠方法模式的擴展性非常優秀
    在增加產品類的情況下,只要適當地修改具體的工廠類或擴展一個工廠類,就可以完成“擁抱變化”
    例如在我們的例子中,需要增加一個棕色人種,則只需要增加一個BrownHuman類,工廠類不用任何修改就可完成系統擴展。
  • 屏蔽產品類
    這一特點非常重要,產品類的實現如何變化,調用者都不需要關心,它只需要關心產品的接口,只要接口保持不變,系統中的上層模塊就不要發生變化
    因爲產品類的實例化工作是由工廠類負責的,一個產品對象具體由哪一個產品生成是由工廠類決定的
    在數據庫開發中,大家應該能夠深刻體會到工廠方法模式的好處:如果使用JDBC連接數據庫,數據庫從MySQL切換到Oracle,需要改動的地方就是切換一下驅動名稱(前提條件是SQL語句是標準語句),其他的都不需要修改,這是工廠方法模式靈活性的一個直接案例。
  • 典型的解耦框架
    高層模塊值需要知道產品的抽象類,其他的實現類都不用關心
    符合迪米特法則,我不需要的就不要去交流
    也符合依賴倒置原則,只依賴產品類的抽象
    當然也符合里氏替換原則,使用產品子類替換產品父類,沒問題!

3.2 缺點


3.3 適用場景


工廠方法模式是new一個對象的替代品

在所有需要生成對象的地方都可以使用,但是需要慎重地考慮是否要增加一個工廠類進行管理,增加代碼的複雜度

需要靈活的、可擴展的框架時

萬物皆對象,那萬物也就皆產品類
例如需要設計一個連接郵件服務器的框架,有三種網絡協議可供選擇:POP3、IMAP、HTTP
我們就可以把這三種連接方法作爲產品類,定義一個接口如IConnectMail
然後定義對郵件的操作方法
用不同的方法實現三個具體的產品類(也就是連接方式)
再定義一個工廠方法,按照不同的傳入條件,選擇不同的連接方式
如此設計,可以做到完美的擴展,如某些郵件服務器提供了WebService接口,很好,我們只要增加一個產品類就可以了

異構項目

例如通過WebService與一個非Java的項目交互,雖然WebService號稱是可以做到異構系統的同構化,但是在實際的開發中,還是會碰到很多問題,如類型問題、WSDL文件的支持問題,等等。從WSDL中產生的對象都認爲是一個產品,然後由一個具體的工廠類進行管理,減少與外圍系統的耦合。

使用在測試驅動開發的框架下

例如,測試一個類A,就需要把與類A有關聯關係的類B也同時產生出來,我們可以使用工廠方法模式把類B虛擬出來,避免類A與類B的耦合。目前由於JMock和EasyMock的誕生,該使用場景已經弱化了,讀者可以在遇到此種情況時直接考慮使用JMock或EasyMock

4 擴展

工廠方法模式有很多擴展,而且與其他模式結合使用威力更大,下面將介紹4種擴展。

4.1 縮小爲簡單工廠模式

我們這樣考慮一個問題:一個模塊僅需要一個工廠類,沒有必要把它產生出來,使用靜態的方法就可以了,根據這一要求,我們把上例中的AbstarctHumanFactory修改一下
簡單工廠模式類圖
我們在類圖中去掉了AbstractHumanFactory抽象類,同時把createHuman方法設置爲靜態類型,簡化了類的創建過程,變更的源碼僅僅是HumanFactory和NvWa類

  • 簡單工廠模式中的工廠類
    待考證

HumanFactory類僅有兩個地方發生變化

  • 去掉繼承抽象類
  • createHuman前增加static關鍵字

工廠類發生變化,也同時引起了調用者NvWa的變化
簡單工廠模式中的場景類
運行結果沒有發生變化,但是我們的類圖變簡單了,而且調用者也比較簡單,該模式是工廠方法模式的弱化,因爲簡單,所以稱爲簡單工廠模式(Simple Factory Pattern),也叫做靜態工廠模式
在實際項目中,採用該方法的案例還是比較多的

  • 其缺點
    工廠類的擴展比較困難,不符合開閉原則,但它仍然是一個非常實用的設計模式。

4.2 升級爲多個工廠類

當我們在做一個比較複雜的項目時,經常會遇到初始化一個對象很耗費精力的情況,所有的產品類都放到一個工廠方法中進行初始化會使代碼結構不清晰
例如,一個產品類有5個具體實現,每個實現類的初始化(不僅僅是new,初始化包括new一個對象,並對對象設置一定的初始值)方法都不相同,如果寫在一個工廠方法中,勢必會導致該方法巨大無比,那該怎麼辦?

考慮到需要結構清晰,我們就爲每個產品定義一個創造者,然後由調用者自己去選擇與哪個工廠方法關聯
我們還是以女媧造人爲例,每個人種都有一個固定的八卦爐,分別造出黑色人種、白色人種、黃色人種
多個工廠類的類圖

  • 每個人種(具體的產品類)都對應了一個創建者,每個創建者都獨立負責創建對應的產品對象,非常符合單一職責原則,看看代碼變化
    多工廠模式的抽象工廠類
    抽象方法中已經不再需要傳遞相關參數了,因爲每一個具體的工廠都已經非常明確自己的職責:創建自己負責的產品類對象。

  • 黑色人種的創建工廠實現

  • 黃色人種的創建類

  • 白色人種的創建類

三個具體的創建工廠都非常簡單,但是,如果一個系統比較複雜時工廠類也會相應地變複雜。

  • 場景類NvWa修改後的代碼

運行結果還是相同
每一個產品類都對應了一個創建類,好處就是創建類的職責清晰,而且結構簡單,但是給可擴展性和可維護性帶來了一定的影響。爲什麼這麼說呢?如果要擴展一個產品類,就需要建立一個相應的工廠類,這樣就增加了擴展的難度。因爲工廠類和產品類的數量相同,維護時需要考慮兩個對象之間的關係。

當然,在複雜的應用中一般採用多工廠的方法,然後再增加一個協調類,避免調用者與各個子工廠交流,協調類的作用是封裝子工廠類,對高層模塊提供統一的訪問接口。

4.3 替代單例模式

單例模式的核心要求就是在內存中只有一個對象,通過工廠方法模式也可以只在內存中生產一個對象
工廠方法模式替代單例模式類圖
Singleton定義了一個private的無參構造函數,目的是不允許通過new的方式創建一個對象
單例類
Singleton保證不能通過正常的渠道建立一個對象

  • 那SingletonFactory如何建立一個單例對象呢?
    反射~
    負責生成單例的工廠類
    通過獲得類構造器,然後設置private訪問權限,生成一個對象,然後提供外部訪問,保證內存中的對象唯一

以上通過工廠方法模式創建了一個單例對象,該框架可以繼續擴展,在一個項目中可以產生一個單例構造器,所有需要產生單例的類都遵循一定的規則(構造方法是private),然後通過擴展該框架,只要輸入一個類型就可以獲得唯一的一個實例。

3.4 延遲初始化(Lazy initialization)

一個對象被消費完畢後,並不立刻釋放,工廠類保持其初始狀態,等待再次被使用
延遲初始化是工廠方法模式的一個擴展應用
延遲初始化的通用類圖
ProductFactory負責產品類對象的創建工作,並且通過prMap變量產生一個緩存,對需要再次被重用的對象保留
延遲加載的工廠類
通過定義一個Map容器,容納所有產生的對象,如果在Map容器中已經有的對象,則直接取出返回;如果沒有,則根據需要的類型產生一個對象並放入到Map容器中,以方便下次調用。

延遲加載框架是可以擴展的,例如限制某一個產品類的最大實例化數量,可以通過判斷Map中已有的對象數量來實現,這樣的處理是非常有意義的,例如JDBC連接數據庫,都會要求設置一個MaxConnections最大連接數量,該數量就是內存中最大實例化的數量。

延遲加載還可以用在對象初始化比較複雜的情況下,例如硬件訪問,涉及多方面的交互,則可以通過延遲加載降低對象的產生和銷燬帶來的複雜性。

4 最佳實踐

工廠方法模式在項目中使用得非常頻繁,以至於很多代碼中都包含工廠方法模式
該模式幾乎盡人皆知,但不是每個人都能用得好。熟能生巧,熟練掌握該模式,多思考工廠方法如何應用,而且工廠方法模式還可以與其他模式混合使用(例如模板方法模式、單例模式、原型模式等),變化出無窮的優秀設計,這也正是軟件設計和開發的樂趣所在。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章