(四)工廠方法模式詳解(另附簡單工廠的死亡之路)

文章開頭首先很是感謝各位的支持,代理模式中提到了class文件(即字節碼文件)的相關知識,有一位讀者說想要看有關class文件的相關內容,我也意識到了這一點,因此之後若是有在講解那個模式的過程中用到了其它的技術,我會留一些篇幅去介紹這個技術,有關class文件的內容我也會看之後的模式當中有沒有用到的地方順便簡單介紹一下,若是沒有的話,我會在設計模式介紹完之後專門寫一篇有關java字節碼文件的相關內容。java

本章咱們繼續討論新的設計模式,工廠方式模式,在這以前,LZ決定先給出引自其它地方的標準定義以及類圖。mysql

定義:工廠方法(Factory Method)模式的意義是定義一個建立產品對象的工廠接口,將實際建立工做推遲到子類當中。核心工廠類再也不負責產品的建立,這樣核心類成爲一個抽象工廠角色,僅負責具體工廠子類必須實現的接口,這樣進一步抽象化的好處是使得工廠方法模式可使系統在不修改具體工廠角色的狀況下引進新的產品。sql

能夠看到工廠方法模式中定義了一個工廠接口,而具體的建立工做推遲到具體的工廠類,它是對簡單工廠模式中的工廠類進一步抽象化,從而產生一個工廠類的抽象和實現體系,從而彌補簡單工廠模式對修改開放的詬病。數據庫

下面LZ給出工廠方法模式的類圖,該類圖和定義引自百度百科。編程

能夠看到,上面右半部分是產品抽象和實現體系,左半部分是工廠抽象和實現體系,其中工廠體系依賴於產品體系,每個工廠負責創造一種產品,這就省去了簡單工廠中的elseif判斷,又客戶端決定實例化一個特定的工廠去建立相應的產品。設計模式

下面LZ簡單的使用JAVA代碼詮釋上述標準的工廠方法模式的類圖。架構

首先是抽象產品接口。oracle

publicinterface Light {
    public void turnOn();
    public void turnOff();
}

下面是具體的產品。app

public class BuldLight implements Light{
    public void turnOn() {
       System.out.println("BuldLight On");
    }
    public void turnOff() {
      System.out.println("BuldLight Off");
    }
}
public class TubeLight implements Light{
    public void turnOn() {
          System.out.println("TubeLight On");
    }
    public void turnOff() {
          System.out.println("TubeLight Off");
    }
}

下面是抽象的工廠接口。框架

public interface Creator {
   public Light createLight();
}

下面是建立指定產品的具體工廠。

public class BuldCreator implements Creator{
    public Light createLight() {
        return new BuldLight();
    }
}  
public class TubeCreator implements Creator{
    public Light createLight() {
       retur nnew TubeLight();
   }
}

下面咱們寫個測試類去實驗一下這個工廠方法模式的實例代碼。

public class Client {
     public static void main(String[] args) {
         Creator creator = new BuldCreator();
         Light light = creator.createLight();
         light.turnOn();
         light.turnOff();
         creator = new TubeCreator();
         light = creator.createLight();
         light.turnOn();
         light.turnOff();
     }
}

 運行結果以下。

  能夠看到,咱們使用能夠隨意的在具體的工廠和產品之間切換,而且不須要修改任何代碼,就可讓原來的程序正常運行,這也是工廠方法模式對擴展開放的表現,另外工廠方法模式彌補了簡單工廠模式不知足開閉原則的詬病,當咱們須要增長產品時,只須要增長相應的產品和工廠類,而不須要修改現有的代碼。    上面的示例能夠比較清楚的展現各個類之間的關係,可是始終缺少說服力,由於它徹底沒有什麼實際意義,下面LZ就給出一些咱們接觸過的例子來講明工廠方法模式的好處。     關於可以說明工廠方法模式的實例,LZ翻遍了全部能找到的源碼,想尋找一個讓各位讀者既能學習到新的東西,又能對工廠方法理解更深的現有的優秀框架的設計。通過跋山涉水,LZ決定仍是拿數據庫鏈接來講事,我知道你想說,我去,又是數據庫鏈接。LZ只想說,咱們天天作的最多的就是增刪改查好嗎,其它的咱也不認識啊,囧。     衆所周知,爲了統一各個數據庫操做的標準,因而有了JDBC的API,它用於給咱們這種被稱做只會使用現成的東西的程序猿,提供一系列統一的,標準化的操做數據庫的接口。其實JDBC的各個類或接口,就是咱們操做數據庫的過程當中各個協助者的抽象,這樣的設計是爲了讓咱們對數據庫的操做依賴於抽象,還記得咱們在設計模式總綱中提到的一句話嗎,用抽象構建框架,用細節擴展實現。

   JDBC API(即抽象的接口或類)就是整個數據庫操做的框架,而各個數據庫的驅動就是那些細節。而咱們的操做依賴於JDBC API,而不是任何一個具體數據庫的細節。

JDBC是如何統一了數據庫世界的呢?其實最主要的就是靠兩個接口,就統一了世界。。。

來看第一個接口Driver,附上源碼。

package java.sql;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;

public interface Driver {
    Connection connect(String url, java.util.Properties info)
       throws SQLException;
     boolean acceptsURL(String url) throws SQLException;

     DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)throws SQLException;
     int getMajorVersion();
     int getMinorVersion();
     boolean jdbcCompliant();
}

因爲篇幅,LZ刪掉了不少註釋,只保留了這個類註釋的第一句話,翻譯過來是這是一個任何驅動類都必須實現的接口。多麼霸氣啊。也就是每一個數據庫廠商都必須實現這個接口來提供JDBC服務,即java數據庫鏈接服務,來方便程序猿對數據庫應用編程。

咱們先忽略掉下面的五個方法,第一個方法毫無疑問是這個接口中相對而講最重要的方法了,即創造一個數據庫鏈接,雖然方法名稱是connect,可是我以爲這個方法徹底能夠改成createConnection。

提到Connction,這個接口咱們必定不陌生,它的源碼也已經在代理模式一章出現過,這裏咱們再次讓它出場,我依舊會刪掉它的大部分方法,限於篇幅。

package java.sql;
import java.sql.PreparedStatement;
import java.sql.SQLException;    
public interface Connection  extends Wrapper {  

   Statement createStatement() throws SQLException;  
   PreparedStatement prepareStatement(String sql) throws SQLException;
}

以上即是Connection接口,這裏只留下了兩個方法,這兩個方法相信各位讀者都很是熟悉,它們都是咱們最常常用的方法之二。

以上兩個接口做爲JDBC API的一部分,它們至關於告訴了數據庫生產廠商兩個要求。

第一,數據庫廠商要提供一個數據庫驅動類,它的做用能夠是能夠創造數據庫鏈接,而這個數據庫鏈接向上轉型爲咱們JDBC的Connection。

** 第二,數據庫廠商要提供一個數據庫鏈接的實現類,這個實現類能夠執行具體數據庫的各個操做,好比幫咱們執行SQL,返回執行結果,關閉鏈接等等。**

咱們都知道mysql的驅動類位於com.mysql.jdbc.Driver,而mysql的connection實現類也在這個包中,名稱是ConnectionImpl,而相應的oracle也有驅動類,位於oracle.jdbc.driver.OracleDriver,相應的oracle也有connection實現類,位於oracle.jdbc.OracleConnectionWrapper。通常每一個數據庫都會有一個Connection的擴展接口,這個接口的做用是提供使用者針對當前數據庫特殊的操做。

這裏咱們忽略掉這些中間接口以及抽象類,我給出上述六個類的UML圖,若是各位之前知道工廠方法模式的話,各位看一下,它們的關係是否很熟悉。

咱們對比上面標準的工廠方法模式,就會發現它們的關係不正是工廠方法模式嗎?

工廠方法模式就是提供一個抽象的工廠,一個抽象的產品,在上述當中至關於Driver(數據庫鏈接工廠)和Connection(抽象產品),實現的一方須要提供一個具體的工廠類(好比mysql驅動)和一個具體的產品(好比mysql數據庫鏈接)。

客戶端調用時不依賴於具體工廠和產品(即究竟是mysql驅動,mysql數據庫鏈接仍是oracle驅動,oracle鏈接,咱們程序猿不須要管的,咱們只管使用抽象的driver和connection,對吧?),而是依賴於抽象工廠和抽象產品完成工做。

各位能夠看到我在類圖裏面加入了一個DriverManager,這個類相信各位也不陌生,這是咱們每天打交道的類,雖然說由於hibernate和ibatis的封裝,或許咱們不能常常看到,但LZ相信它活在每一個程序猿的心中。

DriverMananger在這個設計當中扮演者一個管理者的角色,它幫咱們管理數據庫驅動,讓咱們不須要直接接觸驅動接口,咱們獲取鏈接只須要和DriverManager打交道就能夠,也就是說客戶端依賴於DriverManager和Connection就能夠完成工做,再也不須要與Driver關聯,因此上述說咱們依賴於Driver和Connection,如今DriverManager幫咱們管理Driver,那咱們只須要依賴於DriverManager和Connection就能夠了。

LZ在類圖中拉出了DriverManager的方法,其中的registerDriver方法正是咱們註冊數據庫驅動的入口。來看看mysql的Driver中作了什麼,oracle相似。

public class Driver extends NonRegisteringDriver
  implements java.sql.Driver{
  public Driver() throws SQLException{}
  static{
  try{
      DriverManager.registerDriver(new Driver());
  } catch (SQLException E) {  
       throw new RuntimeException("Can't register driver!");  
  }  
}  
}

能夠看到,在類構造方法中,加入了registerDriver這個方法,因此當咱們使用class.forName加載驅動的時候,將會把mysql驅動註冊到DriverManager,這時DriverManager中就會持有Mysql驅動所必要的信息,咱們就可使用DriverManager來得到具體的mysql鏈接了,固然,你要提供url,用戶名和密碼。

原來咱們都是活在溫室裏的花朵,都被這些設計者細心呵護着,生怕咱們知道一點底層的東西。記得LZ當初第一次看到Class.forName時,還以爲真是個神奇的東西,沒想到只是這些設計者給咱們的糖外衣。

工廠方法模式的好處和適用的場景都相對比較好理解。

好處就是,從類關係上來講,它可讓客戶端與具體的工廠與產品解耦,從業務角度來講,它讓客戶端與具體的產品解耦

適用的場景就是咱們須要一個產品幫咱們完成一項任務,可是這個產品有可能有不少品牌(像這裏的mysql,oracle),爲了保持咱們對產品操做的一致性,咱們就可能要用到工廠方法模式。

工廠方法模式也有它所不足的地方,可能你會說,這多好啊,咱們操縱數據庫再也不須要關心具體是哪一個數據庫。是的,你很爽啊,那是由於這些產品的實現都不用你寫啊,都是數據庫廠商給你寫的。

假設產品數量巨多,並且須要咱們親手去逐個實現的時候,工廠方法模式就會增長系統的複雜性,處處都是工廠類和產品類,並且這裏所說的工廠類和產品類只是概念上的,真正的產品可能不是一兩個類就能搞定,不然mysql和oracle的驅動包爲啥要那麼多類,而不是就一個Driver和一個Connection。

固然這也不是絕對,好比咱們常用的HashSet和ArrayList,也是使用的工廠方法模式,各位看下他們的類圖就看出來了。

各位可能會說,不對啊,這和咱們剛纔理解的不太同樣啊,按照剛纔的說法,咱們不是應該直接使用iterable和iterator嗎?這樣多牛X,咱們不依賴於具體產品了。對於這個LZ表示三條黑線垂下,sun或者說oracle爲了集合框架給你提供了這麼多具有各個特性的集合,你只用iterator和iterable,估計當初參與設計集合框架的人都要氣的去shi了。。

上述這即是工廠方法模式另一種用法了,剛纔由於咱們不關心真正的產品是什麼,因此咱們直接使用抽象接口操做。可是咱們使用iterable和iterator的時候,咱們是關心真正產品的特性的,因此爲了使用產品的特性,咱們就須要使用產品特有的接口了,好比特殊的SortedSet可排序,好比ArrayList能夠有重複元素,能夠根據索引獲取元素等等。固然你依然是可使用iterable和iterator的,可是無論你用什麼,在這種場景下,產品是你本身選的,一句話,你隨便。。。

兩種使用方式一種是對使用者透明的,一種是不透明的,一種是使用者對具體的產品不關心,這種狀況下,通常產品提供的功能是相似的。一種是使用者很是瞭解產品的特性,並想使用產品的特性,這種狀況下,通常產品只提供最基本的一致的功能,但每一個產品都會有本身獨特的一面。

可是LZ我的以爲真正作項目的過程中不多用到工廠方法模式,這個模式更多的是幫助咱們理解現有的開源項目,就像如今,你是否是對JDBC的大致框架有了必定認識了呢,若是你不知道這個模式,可能看源碼會以爲一頭霧水呢。

另外,文章最後插播一段內容,若是各位看過上一章(簡單工廠模式)的話,必定還記得那個噁心的elseif結構,這是簡單工廠的詬病,它對擴展開放,對修改也開放。

簡單工廠模式在項目規模相對較小或者說具體的產品類相對很少的狀況下(針對上章的描述,特指的servlet數量很少的狀況下),其實這種設計仍是能夠接受的,由於少許的elseif能夠換來咱們開發上的便利。

因此LZ建議各位永遠不要忘記,規則只是用來指導你的,不是用來限制你的,只要設計合理,你的設計就是規則

不過針對簡單工廠模式,你能夠認爲它給咱們提供了一個思路,就是咱們其實能夠省掉那些讓人痛恨的xml配置,對於咱們後續的優化有着必定指導意義。

就像上一章中的處理方式,很明顯存在着隱患,那就是在servlet數量急劇上升的時候,工廠類就會變得很是臃腫和複雜,變得難以維護和閱讀。本章LZ給各位讀者介紹一種優化方式,能夠採起一項JDK當中在1.5版本引入的技術,即註解,去消除那些elseif的邏輯判斷。

咱們能夠參考struts2的作法,即每個Servlet咱們均可以採用註解去設置它的名稱,或者叫url,而後咱們讓咱們的簡單工廠依據這個去實例化咱們的servlet。

根據以上方案,咱們須要按照如下步驟讓咱們的簡單工廠完全死翹翹。

1.須要聲明一個註解,它能夠用來給servlet標識它的名稱。

2.須要聲明一個註解的處理器,用來處理咱們的註解,主要做用是經過一個CLASS文件,去得到它的註解信息。

3.基於性能,咱們須要將servlet與名稱的映射與應用的生命週期綁定,而且這份映射在整個應用當中有且僅有一份,且不可更改。

4.讓咱們用於分派請求的過濾器,使用映射信息將客戶請求對應到相應的servlet去處理,而且將分派邏輯移回過濾器,從而完全刪除簡單工廠,即ServletFactory。

特別說一下,這四步當中,其中第三步是可選的,但也是必須的,由於若是不作這種處理,那麼你就等着你的項目N長時間打開一個網頁吧。

以上是簡單工廠給咱們的啓示,具體如何實現這樣一個基於註解的請求分配的架構,LZ再也不給各位一一演示,由於這已經只剩下一個堆積代碼的過程,具體的實現方案已經有了,若是各位讀者有興趣,能夠私底下嘗試一下這種方式。

好了,工廠方法模式就給各位分享到這吧,感謝各位的欣賞。

下期預告,能不能取消這個預告。。。

相關文章
相關標籤/搜索