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


               作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。

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

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

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

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

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


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

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

               首先是抽象產品接口。

public interface Light {

    public void turnOn();

    public void turnOff();
    
}

               下面是具體的產品。

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() {
        return new 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;

/**
 * The interface that every driver class must implement.
 */
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;

/**
 * <P>A connection (session) with a specific
 * database. SQL statements are executed and results are returned
 * within the context of a connection.
 * <P>
 */
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不再給各位一一演示,因為這已經只剩下一個堆積代碼的過程,具體的實現方案已經有了,如果各位讀者有興趣,可以私底下嘗試一下這種方式。

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

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

                

                              

               

               


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM