【創(chuàng)建型模式二】工廠方法(Factory Method)

1 場景問題#

1.1 導出數據的應用框架##

考慮這樣一個實際應用:實現一個導出數據的應用框架,來讓客戶選擇數據的導出方式,并真正執(zhí)行數據導出

在一些實際的企業(yè)應用中,一個公司的系統(tǒng)往往分散在很多個不同的地方運行,比如各個分公司或者是門市點,公司沒有建立全公司專網的實力,但是又不愿意讓業(yè)務數據實時的在廣域網上傳遞,一個是考慮數據安全的問題,一個是運行速度的問題。

這種系統(tǒng)通常會有一個折中的方案,那就是各個分公司內運行系統(tǒng)的時候是獨立的,是在自己分公司的局域網內運行。然后在每天業(yè)務結束的時候,各個分公司會導出自己的業(yè)務數據,然后把業(yè)務數據打包通過網絡傳送給總公司,或是專人把數據送到總公司,然后由總公司進行數據導入和核算。

通常這種系統(tǒng),在導出數據上,會有一些約定的方式,比如導出成:文本格式、數據庫備份形式、Excel格式、Xml格式等等

現在就來考慮實現這樣一個應用框架。在繼續(xù)之前,先來了解一些關于框架的知識。

1.2 框架的基礎知識##

  1. 框架是什么

簡單點說:框架就是能完成一定功能的半成品軟件。

就其本質而言,框架是一個軟件,而且是一個半成品的軟件。所謂半成品,就是還不能完全實現用戶需要的功能,框架只是實現用戶需要的功能的一部分,還需要進一步加工,才能成為一個滿足用戶需要的、完整的軟件。因此框架級的軟件,它的主要客戶是開發(fā)人員,而不是最終用戶。

有些朋友會想,既然框架只是個半成品,那何必要去學習和使用框架呢?學習成本也不算小,那就是因為框架能完成一定的功能,也就是這“框架已經完成的一定的功能”在吸引著開發(fā)人員,讓大家投入去學習和使用框架。

  1. 框架能干什么

能完成一定功能,加快應用開發(fā)進度。

由于框架完成了一定的功能,而且通常是一些基礎的、有難度的、通用的功能,這就避免我們在應用開發(fā)的時候完全從頭開始,而是在框架已有的功能之上繼續(xù)開發(fā),也就是說會復用框架的功能,從而加快應用的開發(fā)進度。

給我們一個精良的程序架構。框架定義了應用的整體結構,包括類和對象的分割,各部分的主要責任,類和對象怎么協作,以及控制流程等等。

現在Java界大多數流行的框架,大都出自大師手筆,設計都很精良。基于這樣的框架來開發(fā),一般會遵循框架已經規(guī)劃好的結構來進行開發(fā),從而讓我們開發(fā)的應用程序的結構也相對變得精良了。

  1. 對框架的理解

基于框架來開發(fā),事情還是那些事情,只是看誰做的問題。

對于應用程序和框架的關系,可以用一個圖來簡單描述一下,如圖所示:

應用程序和框架的關系

如果沒有框架,那么客戶要求的所有功能都由開發(fā)人員自己來開發(fā),沒問題,同樣可以實現用戶要求的功能,只是開發(fā)人員的工作多點。

如果有了框架,框架本身完成了一定的功能,那么框架已有的功能,開發(fā)人員就可以不做了,開發(fā)人員只需要完成框架沒有的功能,最后同樣是完成客戶要求的所有功能,但是開發(fā)人員的工作就減少了。

也就是說,基于框架來開發(fā),軟件要完成的功能并沒有變化,還是客戶要求的所有功能,也就是“事情還是那些事情”的意思。但是有了框架過后,框架完成了一部分功能,然后開發(fā)人員再完成一部分功能,最后由框架和開發(fā)人員合起來完成了整個軟件的功能,也就是看這些功能“由誰做”的問題

基于框架開發(fā),可以不去做框架所做的事情,但是應該明白框架在干什么,以及框架是如何實現相應功能的。

事實上,在實際開發(fā)中,應用程序和框架的關系,通常都不會如上面講述的那樣,分得那么清楚,更為普遍的是相互交互的,也就是應用程序做一部分工作,然后框架做一部分工作,然后應用程序再做一部分工作,然后框架再做一部分工作,如此交錯,最后由應用程序和框架組合起來完成用戶的功能要求。

也用個圖來說明,如圖所示:

應用程序和框架組合起來

如果把這個由應用程序和框架組合在一起構成的矩形,當作最后完成的軟件。試想一下,如果你不懂框架在干什么的話,相當于框架對你來講是個黑盒,也就是相當于在上面圖中,去掉框架的兩塊,會發(fā)現什么?沒錯,剩下的應用程序是支離破碎的,是相互分隔開來的。

這會導致一個非常致命的問題,整個應用是如何運轉起來的,你是不清楚的,也就是說對你而言,項目已經失控了,從項目管理的角度來講,這是很危險的。

因此,在基于框架開發(fā)的時候,雖然我們可以不去做框架所做的事情,但是應該搞明白框架在干什么,如果條件許可的話,還應該搞清楚框架是如何實現相應功能的,至少應該把大致的實現思路和實現步驟搞清楚,這樣我們才能整體的掌控整個項目,才能盡量減少出現項目失控的情況

  1. 框架和設計模式的關系

設計模式比框架更抽象。

框架已經是實現出來的軟件了,雖然只是個半成品的軟件,但畢竟是已經實現出來的了。而設計模式的重心還在于解決問題的方案上,也就是還停留在思想的層面。因此設計模式比框架更為抽象

設計模式是比框架更小的體系結構元素。

如上所述,框架是已經實現出來的軟件,并實現了一系列的功能,因此一個框架,通常會包含多個設計模式的應用。

框架比設計模式更加特例化。

框架是完成一定功能的半成品軟件,也就是說,框架的目的很明確,就是要解決某一個領域的某些問題,那是很具體的功能,不同的領域實現出來的框架是不一樣的。

而設計模式還停留在思想的層面,在不同的領域都可以應用,只要相應的問題適合用某個設計模式來解決。因此框架總是針對特定領域的,而設計模式更加注重從思想上,從方法上來解決問題,更加通用化。

1.3 有何問題##

分析上面要實現的應用框架,不管用戶選擇什么樣的導出格式,最后導出的都是一個文件,而且系統(tǒng)并不知道究竟要導出成為什么樣的文件,因此應該有一個統(tǒng)一的接口,來描述系統(tǒng)最后生成的對象,并操作輸出的文件。

先把導出的文件對象的接口定義出來,示例代碼如下:

/**
 * 導出的文件對象的接口
 */
public interface ExportFileApi {
   /**
    * 導出內容成為文件
    * @param data 示意:需要保存的數據
    * @return 是否導出成功
    */
   public boolean export(String data);
}

對于實現導出數據的業(yè)務功能對象,它應該根據需要來創(chuàng)建相應的ExportFileApi的實現對象,因為特定的ExportFileApi的實現是與具體的業(yè)務相關的。但是對于實現導出數據的業(yè)務功能對象而言,它并不知道應該創(chuàng)建哪一個ExportFileApi的實現對象,也不知道如何創(chuàng)建。

也就是說:對于實現導出數據的業(yè)務功能對象,它需要創(chuàng)建ExportFileApi的具體實例對象,但是它只知道ExportFileApi接口,而不知道其具體的實現。那該怎么辦呢?

2 解決方案#

2.1 工廠方法模式來解決##

用來解決上述問題的一個合理的解決方案就是工廠方法模式。那么什么是工廠方法模式呢?

  1. 工廠方法模式定義

定義一個用于創(chuàng)建對象的接口,讓子類決定實例化哪一個類,Factory Method使一個類的實例化延遲到其子類。

  1. 應用工廠方法模式來解決的思路

仔細分析上面的問題,事實上在實現導出數據的業(yè)務功能對象里面,根本就不知道究竟要使用哪一種導出文件的格式,因此這個對象本就不應該和具體的導出文件的對象耦合在一起,它只需要面向導出的文件對象的接口就好了

但是這樣一來,又有新的問題產生了:接口是不能直接使用的,需要使用具體的接口實現對象的實例

這不是自相矛盾嗎?要求面向接口,不讓和具體的實現耦合,但是又需要創(chuàng)建接口的具體實現對象的實例。怎么解決這個矛盾呢?

工廠方法模式的解決思路很有意思,那就是不解決,采取無為而治的方式:不是需要接口對象嗎,那就定義一個方法來創(chuàng)建;可是事實上它自己是不知道如何創(chuàng)建這個接口對象的,沒有關系,那就定義成抽象方法就好了,自己實現不了,那就讓子類來實現,這樣這個對象本身就可以只是面向接口編程,而無需關心到底如何創(chuàng)建接口對象了

2.2 模式結構和說明##

工廠方法模式的結構如圖所示:

工廠方法模式的結構

Product:定義工廠方法所創(chuàng)建的對象的接口,也就是實際需要使用的對象的接口。

ConcreteProduct:具體的Product接口的實現對象。

Creator:創(chuàng)建器,聲明工廠方法,工廠方法通常會返回一個Product類型的實例對象,而且多是抽象方法。也可以在Creator里面提供工廠方法的默認實現,讓工廠方法返回一個缺省的Product類型的實例對象。

ConcreteCreator:具體的創(chuàng)建器對象,覆蓋實現Creator定義的工廠方法,返回具體的Product實例。

2.3 工廠方法模式示例代碼##

  1. 先看看Product的定義,示例代碼如下:
/**
   * 工廠方法所創(chuàng)建的對象的接口
   */
public interface Product {
      //可以定義Product的屬性和方法
}
  1. 再看看具體的Product的實現對象,示例代碼如下:
/**
   * 具體的Product對象
   */
public class ConcreteProduct implements Product {
      // 實現Product要求的方法
}
  1. 接下來看看創(chuàng)建器的定義,示例代碼如下:
/**
   * 創(chuàng)建器,聲明工廠方法
   */
public abstract class Creator {
      /**
       * 創(chuàng)建Product的工廠方法
       * @return Product對象
       */
      protected abstract Product factoryMethod();
      /**
       * 示意方法,實現某些功能的方法
       */
      public void someOperation() {
          //通常在這些方法實現中,需要調用工廠方法來獲取Product對象
          Product product = factoryMethod();
      }
}
  1. 再看看具體的創(chuàng)建器實現對象,示例代碼如下:
/**
   * 具體的創(chuàng)建器實現對象
   */
public class ConcreteCreator extends Creator {
      protected Product factoryMethod() {
          //重定義工廠方法,返回一個具體的Product對象
          return new ConcreteProduct();
      }
}

2.4 使用工廠方法模式來實現示例##

要使用工廠方法模式來實現示例,先來按照工廠方法模式的結構,對應出哪些是被創(chuàng)建的Product,哪些是Creator。分析要求實現的功能,導出的文件對象接口ExportFileApi就相當于是Product,而用來實現導出數據的業(yè)務功能對象就相當于Creator。把Product和Creator分開過后,就可以分別來實現它們了。

使用工廠方法模式來實現示例的程序結構如圖所示:

工廠方法模式來實現示例的程序結構
  1. 導出的文件對象接口ExportFileApi的實現沒有變化,這里就不去贅述了

  2. 接下來看看接口ExportFileApi的實現,為了示例簡單,只實現導出文本文件格式和數據庫備份文件兩種。先看看導出文本文件格式的實現,示例代碼如下:

/**
   * 導出成文本文件格式的對象
   */
public class ExportTxtFile implements ExportFileApi{
      public boolean export(String data) {
          //簡單示意一下,這里需要操作文件
          System.out.println("導出數據"+data+"到文本文件");
          return true;
      }
}

/**
   * 導出成數據庫備份文件形式的對象
   */
public class ExportDB implements ExportFileApi{
      public boolean export(String data) {
          //簡單示意一下,這里需要操作數據庫和文件
          System.out.println("導出數據"+data+"到數據庫備份文件");
          return true;
      }
}
  1. Creator這邊的實現,首先看看ExportOperate的實現,示例代碼如下:
/**
   * 實現導出數據的業(yè)務功能對象
   */
public abstract class ExportOperate {
      /**
       * 導出文件
       * @param data 需要保存的數據
       * @return 是否成功導出文件
       */
      public boolean export(String data){
          //使用工廠方法
          ExportFileApi api = factoryMethod();
          return api.export(data);
      }
      /**
       * 工廠方法,創(chuàng)建導出的文件對象的接口對象
       * @return 導出的文件對象的接口對象
       */
      protected abstract ExportFileApi factoryMethod();
}
  1. 加入了兩個Creator實現,示例代碼如下:
/**
   * 具體的創(chuàng)建器實現對象,實現創(chuàng)建導出成文本文件格式的對象
   */
public class ExportTxtFileOperate extends ExportOperate{
      protected ExportFileApi factoryMethod() {
          //創(chuàng)建導出成文本文件格式的對象
          return new ExportTxtFile();
      }
}

/**
   * 具體的創(chuàng)建器實現對象,實現創(chuàng)建導出成數據庫備份文件形式的對象
   */
public class ExportDBOperate extends ExportOperate{
      protected ExportFileApi factoryMethod() {
          //創(chuàng)建導出成數據庫備份文件形式的對象
          return new ExportDB();
      }
}
  1. 客戶端直接創(chuàng)建需要使用的Creator對象,然后調用相應的功能方法,示例代碼如下:
public class Client {
      public static void main(String[] args) {
          //創(chuàng)建需要使用的Creator對象
          ExportOperate operate = new ExportDBOperate();
          //調用輸出數據的功能方法
          operate.export("測試數據");
      }
}

3 模式講解#

3.1 認識工廠方法模式##

  1. 模式的功能

工廠方法的主要功能是讓父類在不知道具體實現的情況下,完成自身的功能調用,而具體的實現延遲到子類來實現。

這樣在設計的時候,不用去考慮具體的實現,需要某個對象,把它通過工廠方法返回就好了,在使用這些對象實現功能的時候還是通過接口來操作,這非常類似于IoC/DI的思想,這個在后面給大家稍詳細點介紹一下。

  1. 實現成抽象類

工廠方法的實現中,通常父類會是一個抽象類,里面包含創(chuàng)建所需對象的抽象方法,這些抽象方法就是工廠方法。

這里要注意一個問題,子類在實現這些抽象方法的時候,通常并不是真的由子類來實現具體的功能,而是在子類的方法里面做選擇,選擇具體的產品實現對象

父類里面,通常會有使用這些產品對象來實現一定的功能的方法,而且這些方法所實現的功能通常都是公共的功能,不管子類選擇了何種具體的產品實現,這些方法的功能總是能正確執(zhí)行。

  1. 實現成具體的類

當然也可以把父類實現成為一個具體的類,這種情況下,通常是在父類中提供獲取所需對象的默認實現方法,這樣就算沒有具體的子類,也能夠運行

通常這種情況還是需要具體的子類來決定具體要如何創(chuàng)建父類所需要的對象。也把這種情況稱為工廠方法為子類提供了掛鉤,通過工廠方法,可以讓子類對象來覆蓋父類的實現,從而提供更好的靈活性。

  1. 工廠方法的參數和返回

工廠方法的實現中,可能需要參數,以便決定到底選用哪一種具體的實現。也就是說通過在抽象方法里面?zhèn)鬟f參數,在子類實現的時候根據參數進行選擇,看看究竟應該創(chuàng)建哪一個具體的實現對象

一般工廠方法返回的是被創(chuàng)建對象的接口對象,當然也可以是抽象類或者一個具體的類的實例。

  1. 誰來使用工廠方法創(chuàng)建的對象

這里首先要搞明白一件事情,就是誰在使用工廠方法創(chuàng)建的對象?

事實上,在工廠方法模式里面,應該是Creator中的其它方法在使用工廠方法創(chuàng)建的對象,雖然也可以把工廠方法創(chuàng)建的對象直接提供給Creator外部使用,但工廠方法模式的本意,是由Creator對象內部的方法來使用工廠方法創(chuàng)建的對象,也就是說,工廠方法一般不提供給Creator外部使用

客戶端應該是使用Creator對象,或者是使用由Creator創(chuàng)建出來的對象。對于客戶端使用Creator對象,這個時候工廠方法創(chuàng)建的對象,是Creator中的某些方法使用。對于使用那些由Creator創(chuàng)建出來的對象,這個時候工廠方法創(chuàng)建的對象,是構成客戶端需要的對象的一部分。分別舉例來說明。

① 客戶端使用Creator對象的情況

比如前面的示例,對于“實現導出數據的業(yè)務功能對象”的類ExportOperate,它有一個export的方法,在這個方法里面,需要使用具體的“導出的文件對象的接口對象” ExportFileApi,而ExportOperate是不知道具體的ExportFileApi實現的,那么怎么做的呢?就是定義了一個工廠方法,用來返回ExportFileApi的對象,然后export方法會使用這個工廠方法來獲取它所需要的對象,然后執(zhí)行功能

這個時候的客戶端是怎么做的呢?這個時候客戶端主要就是使用這個ExportOperate的實例來完成它想要完成的功能,也就是客戶端使用Creator對象的情況,簡單描述這種情況下的代碼結構如下:

/**
   * 客戶端使用Creator對象的情況下,Creator的基本實現結構
   */
public abstract class Creator {
      /**
       * 工廠方法,一般不對外
       * @return 創(chuàng)建的產品對象
       */
      protected abstract Product factoryMethod();
      /**
       * 提供給外部使用的方法,
       * 客戶端一般使用Creator提供的這些方法來完成所需要的功能
       */
      public void someOperation(){
          //在這里使用工廠方法
          Product p = factoryMethod();
      }
}

② 客戶端使用由Creator創(chuàng)建出來的對象

另外一種是由Creator向客戶端返回由“工廠方法創(chuàng)建的對象”來構建的對象,這個時候工廠方法創(chuàng)建的對象,是構成客戶端需要的對象的一部分。簡單描述這種情況下的代碼結構如下:

/**
   * 客戶端使用Creator來創(chuàng)建客戶端需要的對象的情況下,Creator的基本實現結構
   */
public abstract class Creator {
      /**
       * 工廠方法,一般不對外,創(chuàng)建一個部件對象
       * @return 創(chuàng)建的產品對象,一般是另一個產品對象的部件
       */
      protected abstract Product1 factoryMethod1();
      /**
       * 工廠方法,一般不對外,創(chuàng)建一個部件對象
       * @return 創(chuàng)建的產品對象,一般是另一個產品對象的部件
       */
      protected abstract Product2 factoryMethod2();
      /**
       * 創(chuàng)建客戶端需要的對象,客戶端主要使用產品對象來完成所需要的功能
       * @return 客戶端需要的對象
       */
      public Product createProduct(){
          //在這里使用工廠方法,得到客戶端所需對象的部件對象
          Product1 p1 = factoryMethod1();
          Product2 p2 = factoryMethod2();
          //工廠方法創(chuàng)建的對象是創(chuàng)建客戶端對象所需要的
          Product p = new ConcreteProduct();
          p.setProduct1(p1);
          p.setProduct2(p2);

          return p;
      }
}

小結一下:在工廠方法模式里面,客戶端要么使用Creator對象,要么使用Creator創(chuàng)建的對象,一般客戶端不直接使用工廠方法。當然也可以直接把工廠方法暴露給客戶端操作,但是一般不這么做。

  1. 工廠方法模式的調用順序示意圖

由于客戶端使用Creator對象有兩種典型的情況,因此調用的順序示意圖也分做兩種情況,先看看客戶端使用由Creator創(chuàng)建出來的對象情況的調用順序示意圖,如圖所示:

客戶端使用由Creator創(chuàng)建出來的對象情況的調用順序示意圖

接下來看看客戶端使用Creator對象時候的調用順序示意圖,如圖所示:

客戶端使用Creator對象時候的調用順序示意圖

3.2 工廠方法模式與IoC/DI##

IoC——Inversion of Control 控制反轉

DI——Dependency Injection 依賴注入

  1. 如何理解IoC/DI

要想理解上面兩個概念,就必須搞清楚如下的問題:

參與者都有誰?

依賴:誰依賴于誰?為什么需要依賴?

注入:誰注入于誰?到底注入什么?

控制反轉:誰控制誰?控制什么?為何叫反轉(有反轉就應該有正轉了)?

依賴注入和控制反轉是同一概念嗎?

(1) 參與者都有誰:

一般有三方參與者,一個是某個對象;一個是IoC/DI的容器;另一個是某個對象的外部資源。

又要名詞解釋一下,某個對象指的就是任意的、普通的Java對象; IoC/DI的容器簡單點說就是指用來實現IoC/DI功能的一個框架程序;對象的外部資源指的就是對象需要的,但是是從對象外部獲取的,都統(tǒng)稱資源,比如:對象需要的其它對象、或者是對象需要的文件資源等等。

(2) 誰依賴于誰:當然是某個對象依賴于IoC/DI的容器

(3) 為什么需要依賴:對象需要IoC/DI的容器來提供對象需要的外部資源

(4) 誰注入于誰:很明顯是IoC/DI的容器 注入 某個對象

(5) 到底注入什么:就是注入某個對象所需要的外部資源

(6) 誰控制誰:當然是IoC/DI的容器來控制對象了

(7) 控制什么:主要是控制對象實例的創(chuàng)建

(8) 為何叫反轉:

反轉是相對于正向而言的,那么什么算是正向的呢?考慮一下常規(guī)情況下的應用程序,如果要在A里面使用C,你會怎么做呢?當然是直接去創(chuàng)建C的對象,也就是說,是在A類中主動去獲取所需要的外部資源C,這種情況被稱為正向的。那么什么是反向呢?就是A類不再主動去獲取C,而是被動等待,等待IoC/DI的容器獲取一個C的實例,然后反向的注入到A類中。

用圖例來說明一下,先看沒有IoC/DI的時候,常規(guī)的A類使用C類的示意圖,如圖所示:

常規(guī)的A類使用C類的示意圖

當有了IoC/DI的容器后,A類不再主動去創(chuàng)建C了,如圖所示:

A類不再主動去創(chuàng)建C

而是被動等待,等待IoC/DI的容器獲取一個C的實例,然后反向的注入到A類中,如圖所示:

等待IoC/DI的容器獲取一個C的實例

(9) 依賴注入和控制反轉是同一概念嗎?

根據上面的講述,應該能看出來,依賴注入和控制反轉是對同一件事情的不同描述,從某個方面講,就是它們描述的角度不同。依賴注入是從應用程序的角度在描述,可以把依賴注入描述完整點:應用程序依賴容器創(chuàng)建并注入它所需要的外部資源;而控制反轉是從容器的角度在描述,描述完整點:容器控制應用程序,由容器反向的向應用程序注入應用程序所需要的外部資源。

(10) 小結一下:

其實IoC/DI對編程帶來的最大改變不是從代碼上,而是從思想上,發(fā)生了“主從換位”的變化。應用程序原本是老大,要獲取什么資源都是主動出擊,但是在IoC/DI思想中,應用程序就變成被動的了,被動的等待IoC/DI容器來創(chuàng)建并注入它所需要的資源了。

這么小小的一個改變其實是編程思想的一個大進步,這樣就有效的分離了對象和它所需要的外部資源,使得它們松散耦合,有利于功能復用,更重要的是使得程序的整個體系結構變得非常靈活

  1. 工廠方法模式和IoC/DI有什么關系呢?

從某個角度講,它們的思想很類似。

上面講了,有了IoC/DI過后,應用程序就不再主動了,而是被動等待由容器來注入資源,那么在編寫代碼的時候,一旦要用到外部資源,就會開一個窗口,讓容器能注入進來,也就是提供給容器使用的注入的途徑,當然這不是我們的重點,就不去細細講了,用setter注入來示例一下,看看使用IoC/DI的代碼是什么樣子,示例代碼如下:

public class A {
      /**
       * 等待被注入進來
       */
      private C c = null;
      /**
       * 注入資源C的方法
       * @param c 被注入的資源
       */
      public void setC(C c){
          this.c = c;
      }
      public void t1(){
          //這里需要使用C,可是又不讓主動去創(chuàng)建C了,怎么辦?
          //反正就要求從外部注入,這樣更省心,
          //自己不用管怎么獲取C,直接使用就好了
          c.tc();
      }
}

接口C的示例代碼如下:

public interface C {
      public void tc();
}

從上面的示例代碼可以看出,現在在A里面寫代碼的時候,凡是碰到了需要外部資源,那么就提供注入的途徑,要求從外部注入,自己只管使用這些對象。

public abstract class A1 {
      /**
       * 工廠方法,創(chuàng)建C1,類似于從子類注入進來的途徑
       * @return C1的對象實例
       */
      protected abstract C1 createC1();
      public void t1(){
          //這里需要使用C1類,可是不知道究竟是用哪一個
          //也就不主動去創(chuàng)建C1了,怎么辦?
          //反正會在子類里面實現,這里不用管怎么獲取C1,直接使用就好了
          createC1().tc();
      }
}

子類的示例代碼如下:

public class A2 extends A1 {
    protected C1 createC1() {
        //真正的選擇具體實現,并創(chuàng)建對象
        return new C2();
    }
}

C1接口和前面C接口是一樣的,C2這個實現類也是空的,只是演示一下,因此就不去展示它們的代碼了。

仔細體會上面的示例,對比它們的實現,尤其是從思想層面上,會發(fā)現工廠方法模式和IoC/DI的思想是相似的,都是“主動變被動”,進行了“主從換位”,從而獲得了更靈活的程序結構

3.3 平行的類層次結構##

  1. 什么是平行的類層次結構呢?

簡單點說,假如有兩個類層次結構,其中一個類層次中的每個類在另一個類層次中都有一個對應的類的結構,就被稱為平行的類層次結構。

舉個例子來說,硬盤對象有很多種,如分成臺式機硬盤和筆記本硬盤,在臺式機硬盤的具體實現上面,又有希捷、西數等不同品牌的實現,同樣在筆記本硬盤上,也有希捷、日立、IBM等不同品牌的實現;硬盤對象具有自己的行為,如硬盤能存儲數據,也能從硬盤上獲取數據,不同的硬盤對象對應的行為對象是不一樣的,因為不同的硬盤對象,它的行為的實現方式是不一樣的。如果把硬盤對象和硬盤對象的行為分開描述,那么就構成了如圖所示的結構:

平行的類層次結構示意圖

硬盤對象是一個類層次,硬盤的行為這邊也是一個類層次,而且兩個類層次中的類是對應的。臺式機西捷硬盤對象就對應著硬盤行為里面的臺式機西捷硬盤的行為;筆記本IBM硬盤就對應著筆記本IBM硬盤的行為,這就是一種典型的平行的類層次結構。

這種平行的類層次結構用來干什么呢?主要用來把一個類層次中的某些行為分離出來,讓類層次中的類把原本屬于自己的職責,委托給分離出來的類去實現,從而使得類層次本身變得更簡單,更容易擴展和復用。

一般來講,分離出去的這些類的行為,會對應著類層次結構來組織,從而形成一個新的類層次結構,相當于原來對象的行為的這么一個類層次結構,而這個層次結構和原來的類層次結構是存在對應關系的,因此被稱為平行的類層次結構

  1. 工廠方法模式跟平行的類層次結構有何關系呢?

可以使用工廠方法模式來連接平行的類層次。

看上面的示例圖,在每個硬盤對象里面,都有一個工廠方法createHDOperate,通過這個工廠方法,客戶端就可以獲取一個跟硬盤對象相對應的行為對象。在硬盤對象的子類里面,會覆蓋父類的工廠方法createHDOperate,以提供跟自身相對應的行為對象,從而自然的把兩個平行的類層次連接起來使用。

3.4 參數化工廠方法##

所謂參數化工廠方法指的就是:通過給工廠方法傳遞參數,讓工廠方法根據參數的不同來創(chuàng)建不同的產品對象,這種情況就被稱為參數化工廠方法。當然工廠方法創(chuàng)建的不同的產品必須是同一個Product類型的。

來改造前面的示例,現在有一個工廠方法來創(chuàng)建ExportFileApi這個產品的對象,但是ExportFileApi接口的具體實現很多,為了方便創(chuàng)建的選擇,直接從客戶端傳入一個參數,這樣在需要創(chuàng)建ExportFileApi對象的時候,就把這個參數傳遞給工廠方法,讓工廠方法來實例化具體的ExportFileApi實現對象。

  1. 先來看Product的接口,就是ExportFileApi接口,跟前面的示例沒有任何變化,為了方便大家查看,這里重復一下,示例代碼如下:
/**
   * 導出的文件對象的接口
   */
public interface ExportFileApi {
      /**
       * 導出內容成為文件
       * @param data 示意:需要保存的數據
       * @return 是否導出成功
       */
      public boolean export(String data); 
}
  1. 同樣提供保存成文本文件和保存成數據庫備份文件的實現,跟前面的示例沒有任何變化,示例代碼如下:
public class ExportTxtFile implements ExportFileApi{
      public boolean export(String data) {
          //簡單示意一下,這里需要操作文件
          System.out.println("導出數據"+data+"到文本文件");
          return true;
      }
}
public class ExportDB implements ExportFileApi{
      public boolean export(String data) {
          //簡單示意一下,這里需要操作數據庫和文件
          System.out.println("導出數據"+data+"到數據庫備份文件");
          return true;
      }
}
  1. 接下來該看看ExportOperate類了,這個類的變化大致如下:

ExportOperate類中的創(chuàng)建產品的工廠方法,通常需要提供默認的實現,不抽象了,也就是變成正常方法。

ExportOperate類也不再定義成抽象類了,因為有了默認的實現,客戶端可能需要直接使用這個對象。

設置一個導出類型的參數,通過export方法從客戶端傳入。

/**
   * 實現導出數據的業(yè)務功能對象
   */
public class ExportOperate {
      /**
       * 導出文件
       * @param type 用戶選擇的導出類型
       * @param data 需要保存的數據
       * @return 是否成功導出文件
       */
      public boolean export(int type,String data){
          //使用工廠方法
          ExportFileApi api = factoryMethod(type);
          return api.export(data);
      }
      /**
       * 工廠方法,創(chuàng)建導出的文件對象的接口對象
       * @param type 用戶選擇的導出類型
       * @return 導出的文件對象的接口對象
       */
      protected ExportFileApi factoryMethod(int type){
          ExportFileApi api = null;
          //根據類型來選擇究竟要創(chuàng)建哪一種導出文件對象
          if(type==1){
              api = new ExportTxtFile();
          }else if(type==2){
              api = new ExportDB();
          }
          return api;
      }
}
  1. 此時的客戶端,非常簡單,直接使用ExportOperate類,示例代碼如下:
public class Client {
      public static void main(String[] args) {
          //創(chuàng)建需要使用的Creator對象
          ExportOperate operate = new ExportOperate();
          //調用輸出數據的功能方法,傳入選擇到處類型的參數
          operate.export(1,"測試數據");
      }
}

測試看看,然后修改一下客戶端的參數,體會一下通過參數來選擇具體的導出實現的過程。這是一種很常見的參數化工廠方法的實現方式,但是也還是有把參數化工廠方法實現成為抽象的,這點要注意,并不是說參數化工廠方法就不能實現成為抽象類了。只是一般情況下,參數化工廠方法,在父類都會提供默認的實現

  1. 擴展新的實現

使用參數化工廠方法,擴展起來會非常容易,已有的代碼都不會改變,只要新加入一個子類來提供新的工廠方法實現,然后在客戶端使用這個新的子類即可。

這種實現方式還有一個有意思的功能,就是子類可以選擇性覆蓋,不想覆蓋的功能還可以返回去讓父類來實現,很有意思。

先擴展一個導出成xml文件的實現,試試看,示例代碼如下:

/**
   * 導出成xml文件的對象
   */
public class ExportXml implements ExportFileApi{
      public boolean export(String data) {
          //簡單示意一下
          System.out.println("導出數據"+data+"到XML文件");
          return true;
      }
}

然后擴展ExportOperate類,來加入新的實現,示例代碼如下:

/**
   * 擴展ExportOperate對象,加入可以導出XML文件
   */
public class ExportOperate2 extends ExportOperate{
      /**
       * 覆蓋父類的工廠方法,創(chuàng)建導出的文件對象的接口對象
       * @param type 用戶選擇的導出類型
       * @return 導出的文件對象的接口對象
       */
      protected ExportFileApi factoryMethod(int type){
          ExportFileApi api = null;
          //可以全部覆蓋,也可以選擇自己感興趣的覆蓋,
          //這里只想添加自己新的實現,其它的不管
          if(type==3){
              api = new ExportXml();
          }else{
              //其它的還是讓父類來實現
              api = super.factoryMethod(type);
          }
          return api;
      }
}

看看此時的客戶端,也非常簡單,只是在變換傳入的參數,示例代碼如下:

public class Client {
    public static void main(String[] args) {
        //創(chuàng)建需要使用的Creator對象
        ExportOperate operate = new ExportOperate2();
        //下面變換傳入的參數來測試參數化工廠方法
        operate.export(1,"Test1");
        operate.export(2,"Test2");
        operate.export(3,"Test3");
    }
}

3.5 工廠方法模式的優(yōu)缺點##

  1. 可以在不知具體實現的情況下編程

工廠方法模式可以讓你在實現功能的時候,如果需要某個產品對象,只需要使用產品的接口即可,而無需關心具體的實現。選擇具體實現的任務延遲到子類去完成。

更容易擴展對象的新版本。

工廠方法給子類提供了一個掛鉤,使得擴展新的對象版本變得非常容易。比如上面示例的參數化工廠方法實現中,擴展一個新的導出Xml文件格式的實現,已有的代碼都不會改變,只要新加入一個子類來提供新的工廠方法實現,然后在客戶端使用這個新的子類即可。

另外這里提到的掛鉤,就是我們經常說的鉤子方法(hook),這個會在后面講模板方法模式的時候詳細點說明。

  1. 連接平行的類層次

工廠方法除了創(chuàng)造產品對象外,在連接平行的類層次上也大顯身手。這個在前面已經詳細講述了。

  1. 具體產品對象和工廠方法的耦合性

在工廠方法模式里面,工廠方法是需要創(chuàng)建產品對象的,也就是需要選擇具體的產品對象,并創(chuàng)建它們的實例,因此具體產品對象和工廠方法是耦合的。

3.6 思考工廠方法模式##

  1. 工廠方法模式的本質

工廠方法模式的本質:延遲到子類來選擇實現。

仔細體會前面的示例,你會發(fā)現,工廠方法模式中的工廠方法,在真正實現的時候,一般是先選擇具體使用哪一個具體的產品實現對象,然后創(chuàng)建這個具體產品對象的示例,然后就可以返回去了。也就是說,工廠方法本身并不會去實現產品接口,具體的產品實現是已經寫好了的,工廠方法只要去選擇實現就好了。

有些朋友可能會說,這不是跟簡單工廠一樣嗎?

確實從本質上講,它們是非常類似的,具體實現上都是在“選擇實現”。但是也存在不同點,簡單工廠是直接在工廠類里面進行“選擇實現”;而工廠方法會把這個工作延遲到子類來實現,工廠類里面使用工廠方法的地方是依賴于抽象而不是具體的實現,從而使得系統(tǒng)更加靈活,具有更好的可維護性和可擴展性。

其實如果把工廠方法模式中的Creator退化一下,只提供工廠方法,而且這些工廠方法還都提供默認的實現,那不就變成了簡單工廠了嗎?比如把剛才示范參數化工廠方法的例子代碼拿過來再簡化一下,你就能看出來,寫得跟簡單工廠是差不多的,示例代碼如下:

工廠方法模式中的Creator退化一下

看完上述代碼,會體會到簡單工廠和工廠方法模式是有很大相似性的了吧,從某個角度來講,可以認為簡單工廠就是工廠方法模式的一種特例,因此它們的本質是類似的,也就不足為奇了。

  1. 對設計原則的體現

工廠方法模式很好的體現了“依賴倒置原則”。

依賴倒置原則告訴我們“要依賴抽象,不要依賴于具體類”,簡單點說就是:不能讓高層組件依賴于低層組件,而且不管高層組件還是低層組件,都應該依賴于抽象

比如前面的示例,實現客戶端請求操作的ExportOperate就是高層組件;而具體實現數據導出的對象就是低層組件,比如ExportTxtFile、ExportDB;而ExportFileApi接口就相當于是那個抽象。

對于ExportOperate來說,它不關心具體的實現方式,它只是“面向接口編程”;對于具體的實現來說,它只關心自己“如何實現接口”所要求的功能。

那么倒置的是什么呢?倒置的是這個接口的“所有權”。事實上,ExportFileApi接口中定義的功能,都是由高層組件ExportOperate來提出的要求,也就是說接口中的功能,是高層組件需要的功能。但是高層組件只是提出要求,并不關心如何實現,而低層組件,就是來真正實現高層組件所要求的接口功能的。因此看起來,低層實現的接口的所有權并不在底層組件手中,而是倒置到高層組件去了

  1. 何時選用工廠方法模式

建議在如下情況中,選用工廠方法模式:

如果一個類需要創(chuàng)建某個接口的對象,但是又不知道具體的實現,這種情況可以選用工廠方法模式,把創(chuàng)建對象的工作延遲到子類去實現。

如果一個類本身就希望,由它的子類來創(chuàng)建所需的對象的時候,應該使用工廠方法模式。

3.7 相關模式##

  1. 工廠方法模式和抽象工廠模式

這兩個模式可以組合使用,具體的放到抽象工廠模式中去講。

  1. 工廠方法模式和模板方法模式

這兩個模式外觀類似,都是有一個抽象類,然后由子類來提供一些實現,但是工廠方法模式的子類專注的是創(chuàng)建產品對象,而模板方法模式的子類專注的是為固定的算法骨架提供某些步驟的實現

這兩個模式可以組合使用,通常在模板方法模式里面,使用工廠方法來創(chuàng)建模板方法需要的對象。

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

推薦閱讀更多精彩內容