工厂方法

工厂方法模式(英語: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. 

参见