工厂方法
工厂方法模式(英语:Factory method pattern)是一种实现了“工厂”概念的面向对象设计模式。就像其他创建型模式一样,它也是处理在不指定对象具体类别的情况下创建对象的问题。工厂方法模式的实质是“定义一个创建对象的接口,但让实现这个接口的类别来决定实例化哪个类别。工厂方法让类别的实例化推迟到子类中进行。”[1]
创建一个对象常常需要复杂的过程,所以不适合包含在一个复合对象中。创建对象可能会导致大量的重复代码,可能会需要复合对象访问不到的信息,也可能提供不了足够级别的抽象,还可能并不是复合对象概念的一部分。工厂方法模式通过定义一个单独的创建对象的方法来解决这些问题。由子类实现这个方法来创建具体类别的对象。
对象创建中的有些过程包括决定创建哪个对象、管理对象的生命周期,以及管理特定对象的建立和销毁的概念。
工厂
在面向对象程序设计中,工厂通常是一个用来创建其他对象的对象。工厂是构造方法的抽象,用来实现不同的分配方案。
工厂对象通常包含一个或多个方法,用来创建这个工厂所能创建的各种类别的对象。这些方法可能接收参数,用来指定对象创建的方式,最后返回创建的对象。
有时,特定类别对象的控制过程比简单地创建一个对象更复杂。在这种情况下,工厂对象就派上用场了。工厂对象可能会动态地创建产品类别的对象,或者从对象池中返回一个对象,或者对所创建的对象进行复杂的配置,或者应用其他的操作。
这些类别的对象很有用。几个不同的设计模式都应用了工厂的概念,并可以使用在很多语言中。例如,在《设计模式》一书中,像工厂方法模式、抽象工厂模式、生成器模式,甚至是单例模式都应用了工厂的概念。
代码举例
例如,有一个Button
类别表示按钮,另有它的两个子类WinButton
和MacButton
分别代表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();
}
}
其他举例
- 在ADO.NET中,IDbCommand.CreateParameter (页面存档备份,存于互联网档案馆)使用工厂方法连接平行的类层次结构。
- 在Qt,QMainWindow::createPopupMenu是在框架中定义的工厂方法,可以在应用代码中重写。
- 在Java中,javax.xml.parsers (页面存档备份,存于互联网档案馆)包使用了几个工厂方法。例如javax.xml.parsers.DocumentBuilderFactory和javax.xml.parsers.SAXParserFactory。
变种
虽然工厂方法模式的背后动机是允许子类选择创建对象的具体类别,但是使用工厂方法模式也有一些其他的好处,其中很多并不依赖于子类。因此,有时候也会创建不使用多态性创建对象的工厂方法,以得到使用工厂方法的其他好处。
工厂“方法”而非工厂“类别”
如果抛开设计模式的范畴,“工厂方法”这个词也可以指作为“工厂”的方法,这个方法的主要目的就是创建对象,而这个方法不一定在单独的工厂类别中。这些方法通常作为静态方法,定义在方法所实例化的类别中。
每个工厂方法都有特定的名称。在许多面向对象的编程语言中,构造方法必须和它所在的类别具有相同的名称,这样的话,如果有多种创建对象的方式(重载)就可能导致歧义。工厂方法没有这种限制,所以可以具有描述性的名称。举例来说,根据两个实数创建一个复数,而这两个实数表示直角坐标或极坐标,如果使用工厂方法,方法的含义就非常清晰了。当工厂方法起到这种消除歧义的作用时,构造方法常常被设置为私有方法,从而强制客户端代码使用工厂方法创建对象。
下面的例子展示了在不同的编程语言中实现复数创建的代码:
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
方法,在方法中创建并返回一个FakeDangerous
(Dangerous
的子类),而这是一个模拟对象。这样,单元测试就可以使用TestFoo
来测试Foo
的功能,从而避免了使用Dangerous
对象带来的副作用。
局限性
使用工厂方法有三个局限,第一个与代码重构有关,另外两个与类别的扩展有关。
- 第一个局限是,重构已经存在的类别会破坏客户端代码。例如,
Complex
类别是一个标准的类别,客户端使用构造方法将其实例化。可能会有很多这样的客户端代码:一旦Complex的编写者意识到Complex c = new Complex(-1, 0);
Complex
的实例化应该使用工厂方法实现,他会将Complex
的构造方法设为私有。而此时使用它的构造方法的客户端代码就都失效了。
- 第二个局限是,因为工厂方法所实例化的类别具有私有的构造方法,所以这些类别就不能扩展了。因为任何子类都必须调用父类的构造方法,但父类的私有构造方法是不能被子类调用的。
- 第三个局限是,如果确实扩展了工厂方法所实例化的类别(例如将构造方法设为保护的,虽然有风险但也是可行的),子类必须具有所有工厂方法的一套实现。例如,在上述
Complex
的例子中,如果Complex
有了一个子类StrangeComplex
,那么StrangeComplex
必须提供属于它自己的所有工厂方法,否则将会返回一个StrangeComplex.fromPolar(1, pi);
Complex
(父类)的实例,而不是所希望的子类实例。但有些语言的反射特性可以避免这个问题。
通过修改底层编程语言,使工厂方法称为第一类别的类别成员,可以缓解这三个问题。[3]
参考文献
- ^ 设计模式:可复用面向对象软件的基础
- ^ Feathers, Michael, Working Effectively with Legacy Code, Upper Saddle River, NJ: Prentice Hall Professional Technical Reference, October 2004, ISBN 978-0-13-117705-5
- ^ 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.
- Martin Fowler; Kent Beck; John Brant; William Opdyke; Don Roberts. Refactoring: Improving the Design of Existing Code. Addison-Wesley. 1999-06. ISBN 0-201-48567-2.
- Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. 1994. ISBN 0-201-63361-2.
- Cox, Brad J. Object-oriented programming: an evolutionary approach. Addison-Wesley. 1986. ISBN 978-0-201-10393-9.
- Cohen, Tal; Gil, Joseph. Better Construction with Factories (PDF). Journal of Object Technology (Bertrand Meyer). 2007, 6 (6): 103 [2007-03-12]. doi:10.5381/jot.2007.6.6.a3. (原始内容存档 (PDF)于2021-03-09).