【白話設計模式一】簡單工廠模式(Simple Factory)

#0 系列目錄#java

#1 場景問題# 你們都知道,在Java應用開發中,要「面向接口編程」。那麼什麼是接口?接口有什麼做用?接口如何使用?一塊兒來回顧一下。數據庫

##1.1 接口回顧##編程

  1. Java中接口的概念

在Java中接口是一種特殊的抽象類,跟通常的抽象類相比,接口裏面的全部方法都是抽象方法,接口裏面的全部屬性都是常量。也就是說,接口裏面是隻有方法定義而不會有任何方法實現。設計模式

  1. 接口用來幹什麼

一般用接口來定義實現類的外觀,也就是實現類的行爲定義,用來約束實現類的行爲。接口就至關於一份契約,根據外部應用須要的功能,約定了實現類應該要實現的功能,可是具體的實現類除了實現接口約定的功能外,還能夠根據須要實現一些其它的功能,這是容許的,也就是說實現類的功能包含但不只限於接口約束的功能api

經過使用接口,能夠實現不相關類的相同行爲,而不需考慮這些類之間的層次關係,接口就是實現類對外的外觀。緩存

  1. 接口的思想

根據接口的做用和用途,濃縮下來,接口的思想就是「封裝隔離」工具

一般提到封裝是指對數據的封裝,可是這裏的封裝是指「對被隔離體的行爲的封裝」,或者是「對被隔離體的職責的封裝」;而隔離指的是外部調用和內部實現,外部調用只能經過接口進行調用,而外部調用是不知道內部具體實現的,也就是說外部調用和內部實現是被接口隔離開的學習

  1. 接口的好處

因爲外部調用和內部實現被接口隔離開了,那麼只要接口不變,內部實現的變化就不會影響到外部應用,從而使得系統更靈活,具備更好的擴展性和可維護性,這也就是所謂「接口是系統可插拔性的保證」這句話的意思。測試

  1. 接口和抽象類的選擇

既然接口是一種特殊的抽象類,那麼在開發中,什麼時候選用接口,什麼時候選用抽象類呢?對於它們的選擇,在開發中是一個很重要的問題,特別總結兩句話給你們:優先選用接口。在以下狀況應選擇抽象類:既要定義子類的行爲,又要爲子類提供公共的功能ui

##1.2 面向接口編程## 面向接口編程是Java編程中的一個重要原則

在Java 程序設計裏面,很是講究層的劃分和模塊的劃分。一般按照三層來劃分Java程序,分別是表現層、邏輯層、數據層,它們之間都要經過接口來通信。

在每個層裏面,又有不少個小模塊,一個小模塊對外也應該是一個總體,那麼一個模塊對外也應該提供接口,其它地方須要使用到這個模塊的功能,都應該經過此接口來進行調用。這也就是常說的「接口是被其隔離部分的外觀」。基本的三層結構如圖1所示:

輸入圖片說明

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

輸入圖片說明

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

所謂組件:從設計上講,組件就是能完成必定功能的封裝體。小到一個類,大到一個系統,均可以稱爲組件,由於一個小系統放到更大的系統裏面去,也就當個組件而已。事實上,從設計的角度看,系統、子系統、模塊、組件等說的實際上是同一回事情,都是完成必定功能的封裝體,只不過功能多少不一樣而已。

繼續剛纔的思路,你們會發現,無論是一層仍是一個模塊或者一個組件,都是一個被接口隔離的總體,那麼下面咱們就不去區分它們,統一認爲都是接口隔離體便可,如圖所示:

輸入圖片說明

既然在Java中須要面向接口編程,那麼在程序中到底如何使用接口,來作到真正的面向接口編程呢?

##1.3 不用模式的解決方案## 回憶一下,之前是如何使用接口的呢,假設有一個接口叫Api,而後有一個實現類Impl實現了它,在客戶端怎麼用這個接口呢?

一般都是在客戶端建立一個Impl的實例,把它賦值給一個Api接口類型的變量,而後客戶端就能夠經過這個變量來操做接口的功能了,此時具體的結構圖如圖:

輸入圖片說明

  1. 先定義接口Api,示例代碼以下:
/**
 * 某個接口(通用的、抽象的、非具體的功能)
 */  
public interface Api {  
   /**
    * 某個具體的功能方法的定義,用test1來演示一下。
    * 這裏的功能很簡單,把傳入的s打印輸出便可
    * @param s 任意想要打印輸出的字符串
    */  
   public void test1(String s);  
}
  1. 既然有了接口,天然就要有實現,定義實現Impl,示例代碼以下:
/**
 * 對接口的實現
 */  
public class Impl implements Api{  
   public void test1(String s) {  
       System.out.println("Now In Impl. The input s=="+s);  
   }  
}
  1. 那麼此時的客戶端怎麼寫呢?按照Java的知識,接口不能直接使用,須要使用接口的實現類,示例代碼以下:
/**
 * 客戶端:測試使用Api接口
 */  
public class Client {  
   public static void main(String[] args) {  
       Api api = new Impl();  
       api.test1("哈哈,沒關係張,只是個測試而已!");  
   }  
}

##1.4 有何問題## 上面寫得沒錯吧,在Java的基礎知識裏面就是這麼學的,難道這有什麼問題嗎?請仔細看位於客戶端的下面這句話:

Api api = new Impl();

而後再想一想接口的功能和思想,發現什麼了?仔細再想一想?

你會發如今客戶端調用的時候,客戶端不但知道了接口,同時還知道了具體的實現就是Impl。而接口的思想是「封裝隔離」,而Impl這個實現類,應該是被接口Api封裝並同客戶端隔離開的,也就是說,客戶端根本就不該該知道具體的實現類是Impl。

有朋友說,那好,我就把Impl從客戶端拿掉,讓Api真正的對實現進行「封裝隔離」,而後咱們仍是面向接口來編程。但是,新的問題出現了,當他把「new Impl()」去掉事後,發現他沒法獲得Api接口對象了,怎麼辦呢?

把這個問題描述一下:**在Java編程中,出現只知接口而不知實現,該怎麼辦?**就像如今的Client,它知道要使用Api接口,可是不知由誰實現,也不知道如何實現,從而得不到接口對象,就沒法使用接口,該怎麼辦呢?

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

  1. 簡單工廠定義

輸入圖片說明

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

分析上面的問題,雖然不能讓模塊外部知道模塊內的具體實現,可是模塊內部是能夠知道實現類的,並且建立接口是須要具體實現類的

那麼幹脆在模塊內部新建一個類,在這個類裏面來建立接口,而後把建立好的接口返回給客戶端,這樣外部應用就只須要根據這個類來獲取相應的接口對象,而後就能夠操做接口定義的方法了。把這樣的對象稱爲簡單工廠,就叫Factory吧。

這樣一來,客戶端就能夠經過這個Factory來獲取須要的接口對象,而後調用接口的方法來實現須要的功能,並且客戶端也不用再關心具體實現了。

##2.2 簡單工廠結構和說明## 簡單工廠的結構如圖所示:

輸入圖片說明

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

Impl:具體實現Api的實現類,可能會有多個

Factory:工廠,選擇合適的實現類來建立Api接口對象

Client:客戶端,經過Factory去獲取Api接口對象,而後面向Api接口編程

##2.3 簡單工廠示例代碼##

  1. 先看看Api的定義,示例代碼以下:
/**
 * 接口的定義,該接口能夠經過簡單工廠來建立
 */  
public interface Api {  
   /**
    * 示意,具體的功能方法的定義
    * @param s 示意,須要的參數
    */  
   public void operation(String s);  
}
  1. 定義了接口,該來實現它了,ImplA的示例代碼以下:
/**
 * 接口的具體實現對象A
 */  
public class ImplA implements Api{  
   public void operation(String s) {  
       //實現功能的代碼,示意一下  
       System.out.println("ImplA s=="+s);  
   }  
}  

/**
 * 接口的具體實現對象B
 */  
public class ImplB implements Api{  
   public void operation(String s) {  
       //實現功能的代碼,示意一下  
       System.out.println("ImplB s=="+s);  
   }  
}
  1. 該來看看簡單工廠的實現,示例代碼以下:
/**
 * 工廠類,用來創造Api對象
 */  
public class Factory {  
   /**
    * 具體的創造Api對象的方法
    * @param condition 示意,從外部傳入的選擇條件
    * @return 創造好的Api對象
    */  
   public static Api createApi(int condition){  
       //應該根據某些條件去選擇究竟建立哪個具體的實現對象,  
       //這些條件能夠從外部傳入,也能夠從其它途徑獲取。  
       //若是隻有一個實現,能夠省略條件,由於沒有選擇的必要。  
 
       //示意使用條件  
       Api api = null;  
       if(condition == 1){  
           api = new ImplA();  
       }else if(condition == 2){  
           api = new ImplB();  
       }  
       return api;  
   }  
}
  1. 再來看看客戶端的示意,示例代碼以下:
/**
 * 客戶端,使用Api接口
 */  
public class Client {  
   public static void main(String[] args) {  
       //經過簡單工廠來獲取接口對象  
       Api api = Factory.createApi(1);  
       api.operation("正在使用簡單工廠");  
   }  
}

##2.4 使用簡單工廠重寫示例## 要使用簡單工廠來重寫前面的示例,主要就是要建立一個簡單工廠對象,讓簡單工廠來負責建立接口對象。而後讓客戶端經過工廠來獲取接口對象,而再也不由客戶端本身去建立接口的對象了。此時系統的結構如圖所示:

輸入圖片說明

  1. 接口Api和實現類Impl都和前面的示例同樣,就不去贅述了。

  2. 新建立一個簡單工廠的對象,示例代碼以下:

/**
 * 工廠類,用來創造Api對象
 */  
public class Factory {  
   /**
    * 具體的創造Api對象的方法
    * @return 創造好的Api對象
    */  
   public static Api createApi(){  
       //因爲只有一個實現,就不用條件判斷了  
       return new Impl();  
   }  
}
  1. 客戶端如何使用簡單工廠提供的功能呢?這個時候,客戶端就不用再本身去建立接口的對象了,應該使用工廠來獲取,通過改造,客戶端代碼以下:
/**
 * 客戶端:測試使用Api接口
 */  
public class Client {  
   public static void main(String[] args) {  
       //重要改變,沒有new Impl()了,取而代之Factory.createApi()  
       Api api = Factory.createApi();  
       api.test1("哈哈,沒關係張,只是個測試而已!");  
   }  
}

就如同上面的示例,客戶端經過簡單工廠建立了一個實現接口的對象,而後面向接口編程,從客戶端來看,它根本就不知道具體的實現是什麼,也不知道是如何實現的,它只知道經過工廠得到了一個接口對象,而後就能經過這個接口來獲取想要的功能

事實上,簡單工廠能幫助咱們真正開始面向接口編程,像之前的作法,其實只是用到了接口的多態那部分的功能,最重要的「封裝隔離性」並無體現出來。

#3 模式講解# ##3.1 典型疑問## 首先來解決一個常見的疑問:可能有朋友會認爲,上面示例中的簡單工廠看起來不就是把客戶端裏面的「new Impl()」移動到簡單工廠裏面嗎?不仍是同樣經過new一個實現類來獲得接口嗎?把「new Impl()」這句話放到客戶端和放到簡單工廠裏面有什麼不一樣嗎?

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

根據前面的學習,咱們知道接口是用來封裝隔離具體的實現的,目標就是不要讓客戶端知道封裝體內部的具體實現。簡單工廠的位置是位於封裝體內的,也就是簡單工廠是跟接口和具體的實如今一塊兒的,算是封裝體內部的一個類,因此簡單工廠知道具體的實現類是沒有關係的。整理一下簡單工廠的結構圖,新的圖如圖所示:

輸入圖片說明

圖中虛線框,就比如是一個組件的包裝邊界,表示接口、實現類和工廠類組合成了一個組件,在這個封裝體裏面,只有接口和工廠是對外的,也就是讓外部知道並使用的,因此故意漏了一些在虛線框外,而具體的實現類是不對外的,被徹底包含在虛線框內。

對於客戶端而言,只是知道了接口Api和簡單工廠Factory,經過Factory就能夠得到Api了,這樣就達到了讓Client在不知道具體實現類的狀況下獲取接口Api。因此看似簡單的把「new Impl()」這句話從客戶端裏面移動到了簡單工廠裏面,實際上是有了質的變化的。

##3.2 認識簡單工廠##

  1. 簡單工廠的功能

工廠嘛,就是用來造東西的。在Java裏面,一般狀況下是用來造接口的,可是也能夠造抽象類,甚至是一個具體的類實例。必定要注意,雖然前面的示例是利用簡單工廠來建立的接口,可是也是能夠用簡單工廠來建立抽象類或者是普通類的實例的

  1. 靜態工廠

使用簡單工廠的時候,一般不用建立簡單工廠類的類實例,沒有建立實例的必要。所以能夠把簡單工廠類實現成一個工具類,直接使用靜態方法就能夠了,也就是說簡單工廠的方法一般都是靜態的,因此也被稱爲靜態工廠。若是要防止客戶端無謂的創造簡單工廠實例,還能夠把簡單工廠的構造方法私有化了。

  1. 萬能工廠

一個簡單工廠能夠包含不少用來構造東西的方法,這些方法能夠創造不一樣的接口、抽象類或者是類實例,一個簡單工廠理論上能夠構造任何東西,因此又稱之爲「萬能工廠」

雖然上面的實例中,在簡單工廠裏面只有一個方法,但事實上,是能夠有不少這樣建立方法的,這點要注意。

  1. 簡單工廠建立對象的範圍

雖然從理論上講,簡單工廠什麼都能造,但對於簡單工廠可建立對象的範圍,一般不要太大,建議控制在一個獨立的組件級別或者一個模塊級別,也就是一個組件或模塊一個簡單工廠。不然這個簡單工廠類會職責不明,有點大雜燴的感受。

  1. 簡單工廠的調用順序示意圖

輸入圖片說明

  1. 簡單工廠命名的建議

類名建議爲「模塊名稱+Factory」,好比:用戶模塊的工廠就稱爲:UserFactory

方法名稱一般爲「get+接口名稱」或者是「create+接口名稱」,好比:有一個接口名稱爲UserEbi,那麼方法名稱一般爲:getUserEbi 或者是 createUserEbi。

固然,也有一些朋友習慣於把方法名稱命名爲「new+接口名稱」,好比:newUserEbi,咱們不是很建議。由於new在Java中表明特定的含義,並且經過簡單工廠的方法來獲取對象實例,並不必定每次都是要new一個新的實例。若是使用newUserEbi,這會給人錯覺,好像每次都是new一個新的實例同樣

##3.3 簡單工廠中方法的寫法## 雖說簡單工廠的方法可能是用來造接口的,可是仔細分析就會發現,真正能實現功能的是具體的實現類,這些實現類是已經作好的,並非真的靠簡單工廠來創造出來的,簡單工廠的方法無外乎就是:實現了選擇一個合適的實現類來使用

因此簡單工廠方法的內部主要實現的功能是「選擇合適的實現類」來建立實例對象。既然要實現選擇,那麼就須要選擇的條件或者是選擇的參數,選擇條件或者是參數的來源一般又有幾種:

來源於客戶端,由Client來傳入參數

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

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

下面來看個示例,看看由客戶端來傳入參數,如何寫簡單工廠中的方法。

  1. 在剛纔的示例上再添加一個實現,稱爲Impl2,示例代碼以下:
/**
 * 對接口的一種實現
 */  
public class Impl2 implements Api{  
   public void test1(String s) {  
       System.out.println("Now In Impl The input s=="+s);  
   }  
}
  1. 如今對Api這個接口,有了兩種實現,那麼工廠類該怎麼辦呢?到底如何選擇呢?不可能兩個同時使用吧,看看新的工廠類,示例代碼以下:
/**
 * 工廠類,用來創造Api的
 */  
public class Factory {  
   /**
    * 具體的創造Api的方法,根據客戶端的參數來建立接口
    * @param type 客戶端傳入的選擇創造接口的條件
    * @return 創造好的Api對象
    */  
   public static Api createApi(int type){  
       //這裏的type也能夠不禁外部傳入,而是直接讀取配置文件來獲取  
       //爲了把注意力放在模式自己上,這裏就不去寫讀取配置文件的代碼了  
    
       //根據type來進行選擇,固然這裏的1和2應該作成常量  
       Api api = null;  
       if(type==1){  
           api = new Impl();  
       }else if(type==2){  
           api = new Impl2();  
       }  
       return api;  
   }  
}
  1. 客戶端沒有什麼變化,只是在調用Factory的createApi方法的時候須要傳入參數,示例代碼以下:
public class Client {  
   public static void main(String[] args) {  
       //注意這裏傳遞的參數,修改參數就能夠修改行爲,試試看吧  
       Api api = Factory.createApi(2);  
       api.test1("哈哈,沒關係張,只是個測試而已!");  
   }  
}
  1. 要注意這種方法有一個缺點

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

##3.4 可配置的簡單工廠## 如今已經學會經過簡單工廠來選擇具體的實現類了,但是還有問題。好比:在如今的實現中,再新增長一種實現,會怎樣呢?

那就須要修改工廠類,才能把新的實現添加到現有系統中。好比如今新加了一個實現Impl3,那麼須要相似下面這樣來修改工廠類:

public class Factory {  
   public static Api createApi(int type){  
       Api api = null;  
       if(type==1){  
           api = new Impl();  
       }else if(type==2){  
           api = new Impl2();  
       }  
 
       else if(type==3){  
           api = new Impl3();  
       }  
       return api;  
   }  
}

每次新增長一個實現類都來修改工廠類的實現,確定不是一個好的實現方式。那麼如今但願新增長了實現類事後不修改工廠類,該怎麼辦呢?

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

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

  1. **配置文件用最簡單的properties文件,實際開發中可能是xml配置。定義一個名稱爲「FactoryTest.properties」的配置文件,放置到Factory同一個包下面,內容以下: **
ImplClass=cn.javass.dp.simplefactory.example5.Impl

若是新添加了實現類,修改這裏的配置就能夠了,就不須要修改程序了。

  1. 此時的工廠類實現以下:
/**
 * 工廠類,用來創造Api對象
 */  
public class Factory {  
  /**
   * 具體的創造Api的方法,根據配置文件的參數來建立接口
   * @return 創造好的Api對象
   */  
  public static Api createApi(){  
      //直接讀取配置文件來獲取須要建立實例的類  
      //至於如何讀取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();  
          }  
      }  
      //用反射去建立,那些例外處理等完善的工做這裏就不作了  
      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;  
  }  
}
  1. 此時的客戶端就變得很簡單了,再也不須要傳入參數,代碼示例以下:
public class Client {  
  public static void main(String[] args) {  
      Api api = Factory.createApi();  
      api.test1("哈哈,沒關係張,只是個測試而已!");  
  }  
}

##3.5 簡單工廠的優缺點## **幫助封裝:**簡單工廠雖然很簡單,可是很是友好的幫助咱們實現了組件的封裝,而後讓組件外部能真正面向接口編程。

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

**可能增長客戶端的複雜度:**若是經過客戶端的參數來選擇具體的實現類,那麼就必須讓客戶端能理解各個參數所表明的具體功能和含義,這會增長客戶端使用的難度,也部分暴露了內部實現,這種狀況能夠選用可配置的方式來實現。

**不方便擴展子工廠:**私有化簡單工廠的構造方法,使用靜態方法來建立接口,也就不能經過寫簡單工廠類的子類來改變建立接口的方法的行爲了。不過,一般狀況下是不須要爲簡單工廠建立子類的。

##3.6 思考簡單工廠##

  1. 簡單工廠的本質

簡單工廠的本質是:選擇實現。

注意簡單工廠的重點在選擇,實現是已經作好了的。就算實現再簡單,也要由具體的實現類來實現,而不是在簡單工廠裏面來實現。簡單工廠的目的在於爲客戶端來選擇相應的實現,從而使得客戶端和實現之間解耦,這樣一來,具體實現發生了變化,就不用變更客戶端了,這個變化會被簡單工廠吸取和屏蔽掉。

實現簡單工廠的難點就在於「如何選擇」實現,前面講到了幾種傳遞參數的方法,那都是靜態的參數,還能夠實現成爲動態的參數。好比:在運行期間,由工廠去讀取某個內存的值,或者是去讀取數據庫中的值,而後根據這個值來選擇具體的實現等等。

  1. 什麼時候選用簡單工廠

建議在以下狀況中,選用簡單工廠:若是想要徹底封裝隔離具體實現,讓外部只能經過接口來操做封裝體,那麼能夠選用簡單工廠,讓客戶端經過工廠來獲取相應的接口,而無需關心具體實現;若是想要把對外建立對象的職責集中管理和控制,能夠選用簡單工廠,一個簡單工廠能夠建立不少的、不相關的對象,能夠把對外建立對象的職責集中到一個簡單工廠來,從而實現集中管理和控制。

##3.7 相關模式##

  1. 簡單工廠和抽象工廠模式

簡單工廠是用來選擇實現的,能夠選擇任意接口的實現,一個簡單工廠能夠有多個用於選擇並建立對象的方法,多個方法建立的對象能夠有關係也能夠沒有關係。

抽象工廠模式是用來選擇產品簇的實現的,也就是說通常抽象工廠裏面有多個用於選擇並建立對象的方法,可是這些方法所建立的對象之間一般是有關係的,這些被建立的對象一般是構成一個產品簇所須要的部件對象。

因此從某種意義上來講,簡單工廠和抽象工廠是相似的,若是抽象工廠退化成爲只有一個實現,不分層次,那麼就至關於簡單工廠了

  1. 簡單工廠和工廠方法模式

    簡單工廠和工廠方法模式也是很是相似的。工廠方法的本質也是用來選擇實現的,跟簡單工廠的區別在於工廠方法是把選擇具體實現的功能延遲到子類去實現。若是把工廠方法中選擇的實現放到父類直接實現,那就等同於簡單工廠。

  2. 簡單工廠和能建立對象實例的模式

簡單工廠的本質是選擇實現,因此它能夠跟其它任何可以具體的建立對象實例的模式配合使用,好比:單例模式、原型模式、生成器模式等等。

相關文章
相關標籤/搜索