工廠方法

工廠方法模式(英語:Factory method pattern)是一種實現了「工廠」概念的面向對象設計模式。就像其他創建型模式一樣,它也是處理在不指定對象具體類別的情況下創建對象的問題。工廠方法模式的實質是「定義一個創建對象的接口,但讓實現這個接口的類別來決定實例化哪個類別。工廠方法讓類別的實例化推遲到子類別中進行。」[1]

UML描述的工廠方法模式
LePUS3描述的工廠方法模式

創建一個對象常常需要複雜的過程,所以不適合包含在一個複合對象中。創建對象可能會導致大量的重複代碼,可能會需要複合對象訪問不到的信息,也可能提供不了足夠級別的抽象,還可能並不是複合對象概念的一部分。工廠方法模式通過定義一個單獨的創建對象的方法來解決這些問題。由子類別實現這個方法來創建具體類別的對象。

對象創建中的有些過程包括決定創建哪個對象、管理對象的生命周期,以及管理特定對象的建立和銷毀的概念。

工廠

面向對象程序設計中,工廠通常是一個用來創建其他對象的對象。工廠是構造方法抽象,用來實現不同的分配方案。

工廠對象通常包含一個或多個方法,用來創建這個工廠所能創建的各種類別的對象。這些方法可能接收參數,用來指定對象創建的方式,最後返回創建的對象。

有時,特定類別對象的控制過程比簡單地創建一個對象更複雜。在這種情況下,工廠對象就派上用場了。工廠對象可能會動態地創建產品類別的對象,或者從對象池中返回一個對象,或者對所創建的對象進行複雜的配置,或者應用其他的操作。

這些類別的對象很有用。幾個不同的設計模式都應用了工廠的概念,並可以使用在很多語言中。例如,在《設計模式》一書中,像工廠方法模式抽象工廠模式生成器模式,甚至是單例模式都應用了工廠的概念。

代碼舉例

例如,有一個Button類別表示按鈕,另有它的兩個子類別WinButtonMacButton分別代表Windows和Mac風格的按鈕,那麼這幾個類別和用於創建它們的工廠類別在Java中可以如下實現(在此省略所有類別和方法的可見性設置):

//幾個Button類
class Button{/* ...*/}
class WinButton extends Button{/* ...*/}
class MacButton extends Button{/* ...*/}

//他們的工廠類別
interface ButtonFactory {
    abstract Button createButton();
}
class WinButtonFactory implements ButtonFactory {
    Button createButton() {
        return new WinButton();
    }
}
class MacButtonFactory implements ButtonFactory {
    Button createButton() {
        return new MacButton();
    }
}

其他舉例

變種

雖然工廠方法模式的背後動機是允許子類別選擇創建對象的具體類別,但是使用工廠方法模式也有一些其他的好處,其中很多並不依賴於子類別。因此,有時候也會創建不使用多態性創建對象的工廠方法,以得到使用工廠方法的其他好處。

工廠「方法」而非工廠「類別」

如果拋開設計模式的範疇,「工廠方法」這個詞也可以指作為「工廠」的方法,這個方法的主要目的就是創建對象,而這個方法不一定在單獨的工廠類別中。這些方法通常作為靜態方法,定義在方法所實例化的類別中。

每個工廠方法都有特定的名稱。在許多面向對象的編程語言中,構造方法必須和它所在的類別具有相同的名稱,這樣的話,如果有多種創建對象的方式(重載)就可能導致歧義。工廠方法沒有這種限制,所以可以具有描述性的名稱。舉例來說,根據兩個實數創建一個複數,而這兩個實數表示直角坐標或極坐標,如果使用工廠方法,方法的含義就非常清晰了。當工廠方法起到這種消除歧義的作用時,構造方法常常被設置為私有方法,從而強制客戶端代碼使用工廠方法創建對象。

下面的例子展示了在不同的編程語言中實現複數創建的代碼:

Java

class Complex {
     public static Complex fromCartesianFactory(double real, double imaginary) {
         return new Complex(real, imaginary);
     }
     public static Complex fromPolarFactory(double modulus, double angle) {
         return new Complex(modulus * cos(angle), modulus * sin(angle));
     }
     private Complex(double a, double b) {
         //...
     }
}

Complex product = Complex.fromPolarFactory(1, pi);

VB.NET

Public Class Complex
    Public Shared Function fromCartesianFactory(real As Double, imaginary As Double) As Complex
        Return (New Complex(real, imaginary))
    End Function

    Public Shared Function fromPolarFactory(modulus As Double, angle As Double) As Complex
        Return (New Complex(modulus * Math.Cos(angle), modulus * Math.Sin(angle)))
    End Function

    Private Sub New(a As Double, b As Double)
        '...
    End Sub
End Class

Complex product = Complex.fromPolarFactory(1, pi);

C#

public class Complex
{
    public double Real;
    public double Imaginary;

    public static Complex FromCartesianFactory(double real, double imaginary) 
    {
        return new Complex(real, imaginary);
    }

    public static Complex FromPolarFactory(double modulus, double angle) 
    {
        return new Complex(modulus * Math.Cos(angle), modulus * Math.Sin(angle));
    }
 
    private Complex (double real, double imaginary)
    {
        Real = real;
        Imaginary = imaginary;
    }
}

var product = Complex.FromPolarFactory(1, pi);

簡單工廠

普通的工廠方法模式通常伴隨着對象的具體類別與工廠具體類別的一一對應,客戶端代碼根據需要選擇合適的具體類別工廠使用。然而,這種選擇可能包含複雜的邏輯。這時,可以創建一個單一的工廠類別,用以包含這種選擇邏輯,根據參數的不同選擇實現不同的具體對象。這個工廠類別不需要由每個具體產品實現一個自己的具體的工廠類別,所以可以將工廠方法設置為靜態方法。 而且,工廠方法封裝了對象的創建過程。如果創建過程非常複雜(比如依賴於配置文件或用戶輸入),工廠方法就非常有用了。 比如,一個程序要讀取圖像文件。程序支持多種圖像格式,每種格式都有一個對應的ImageReader類別用來讀取圖像。程序每次讀取圖像時,需要基於文件信息創建合適類別的ImageReader。這個選擇邏輯可以包裝在一個簡單工廠中:

public class ImageReaderFactory {
    public static ImageReader imageReaderFactoryMethod(InputStream is) {
        ImageReader product = null;

        int imageType = determineImageType(is);
        switch (imageType) {
            case ImageReaderFactory.GIF:
                product = new GifReader(is);
            case ImageReaderFactory.JPEG:
                product = new JpegReader(is);
            //...
        }
        return product;
    }
}

適用性

工廠方法,適用於面向接口編程(programming to interface)與實現依賴反轉原則。 下列情況可以考慮使用工廠方法模式:

  • 創建對象需要大量重複的代碼。可以把這些代碼寫在工廠基類別中。
  • 創建對象需要訪問某些信息,而這些信息不應該包含在複合類別中。
  • 創建對象的生命周期必須集中管理,以保證在整個程序中具有一致的行為。 對象創建時會有很多參數來決定如何創建出這個對象。
  • 創建對象可能是一個pool里的,不是每次都憑空創建一個新的。而pool的大小等參數可以用另外的邏輯去控制。比如連接池對象,線程池對象
  • 業務對象的代碼作者希望隱藏對象的真實類別,而構造函數一定要真實的類別名才能用
  • 簡化一些常規的創建過程。根據配置去創建一個對象也很複雜;但可能95%的情況只創建某個特定類別的對象。這時可以弄個函數直接省略那些配置過程。如Java的線程池的相關創建api(如Executors.newFixedThreadPool等)
  • 創建一個對象有複雜的依賴關係,比如Foo對象的創建依賴A,A又依賴B,B又依賴C……。於是創建過程是一組對象的的創建和注入。
  • 知道怎麼創建一個對象,但是無法把控創建的時機。需要把「如何創建」的代碼塞給「負責決定什麼時候創建」的代碼。後者在適當的時機,回調執行創建對象的函數。在支持用函數作為一等公民傳參的語言,比如js,go等,直接用創建函數就行了。對於java需要搞個XXXXFactory的類別去傳。
  • 構造函數裡不要拋出異常

工廠方法模式常見於工具包和框架中,在這些庫中可能需要創建客戶端代碼實現的具體類別的對象。

平行的類別層次結構中,常常需要一個層次結構中的對象能夠根據需要創建另一個層次結構中的對象。

工廠方法模式可以用於測試驅動開發,從而允許將類別放在測試中[2]。舉例來說,Foo這個類別創建了一個Dangerous對象,但是Dangerous對象不能放在自動的單元測試中(可能它需要訪問產品數據庫,而這個數據庫不是隨時能夠訪問到的)。所以,就可以把Dangerous對象的創建交由Foo類別的一個方法(虛函數createDangerous完成。為了測試,再創建一個Foo的一個子類別TestFoo,重寫createDangerous方法,在方法中創建並返回一個FakeDangerousDangerous的子類別),而這是一個模擬對象。這樣,單元測試就可以使用TestFoo來測試Foo的功能,從而避免了使用Dangerous對象帶來的副作用。

局限性

使用工廠方法有三個局限,第一個與代碼重構有關,另外兩個與類別的擴展有關。

  • 第一個局限是,重構已經存在的類別會破壞客戶端代碼。例如,Complex類別是一個標準的類別,客戶端使用構造方法將其實例化。可能會有很多這樣的客戶端代碼:
    Complex c = new Complex(-1, 0);
    
    一旦Complex的編寫者意識到Complex的實例化應該使用工廠方法實現,他會將Complex的構造方法設為私有。而此時使用它的構造方法的客戶端代碼就都失效了。
  • 第二個局限是,因為工廠方法所實例化的類別具有私有的構造方法,所以這些類別就不能擴展了。因為任何子類別都必須調用父類別的構造方法,但父類別的私有構造方法是不能被子類別調用的。
  • 第三個局限是,如果確實擴展了工廠方法所實例化的類別(例如將構造方法設為保護的,雖然有風險但也是可行的),子類別必須具有所有工廠方法的一套實現。例如,在上述Complex的例子中,如果Complex有了一個子類別StrangeComplex,那麼StrangeComplex必須提供屬於它自己的所有工廠方法,否則
    StrangeComplex.fromPolar(1, pi);
    
    將會返回一個Complex(父類別)的實例,而不是所希望的子類別實例。但有些語言的反射特性可以避免這個問題。

通過修改底層編程語言,使工廠方法稱為第一類別的類別成員,可以緩解這三個問題。[3]

參考文獻

  1. ^ 設計模式:可復用面向對象軟件的基礎
  2. ^ Feathers, Michael, Working Effectively with Legacy Code, Upper Saddle River, NJ: Prentice Hall Professional Technical Reference, October 2004, ISBN 978-0-13-117705-5 
  3. ^ Agerbo, Ellen; Cornils, Aino. How to preserve the benefits of design patterns. Conference on Object Oriented Programming Systems Languages and Applications (Vancouver, British Columbia, Canada: ACM). 1998: 134–143. ISBN 1-58113-005-8. 

參見