浅谈设计模式之工厂方法模式

优美图

工厂模式

首先需要先说一下工厂模式,工厂模式根据抽象程度的不同分为三种:简单工厂模式(也叫静态工厂模式)、工厂方法模式、以及抽象工厂模式。在说完抽象工厂模式之后,我们再来区分一下三者的不同。

工厂模式的主要优点有:

  • 可以使代码结构清晰,有效地封装变化。在编程中,产品类的实例化有时候是比较复杂和多变的,通过工厂模式,将产品的实例化封装起来,使得调用者根本无需关心产品的实例化过程,只需依赖工厂即可得到自己想要的产品。
  • 对调用者屏蔽具体的产品类。如果使用工厂模式,调用者只关心产品的接口就可以了,至于具体的实现,调用者根本无需关心。即使变更了具体的实现,对调用者来说没有任何影响。
  • 降低耦合度。产品类的实例化通常来说是很复杂的,它需要依赖很多的类,而这些类对于调用者来说根本无需知道,如果使用了工厂方法,我们需要做的仅仅是实例化好产品类,然后交给调用者使用。对调用者来说,产品所依赖的类都是透明的。

工厂方法模式(Factory method pattern)

今天我们的主角是工厂方法模式。为什么不先介绍简单工厂模式呢?因为它不在GoF提到的23种设计模式里,而且内容较为简单难以说成一章,所以只放在后面的区别里会简单介绍一下。

首先先看一下Wiki给我们的两张图片。

用UML描述的工厂方法模式:

UML

用LePUS3描述的工厂方法模式:

LePUS3

可能有点抽象,接下来再来展示我从网上找到的具体一点的类图。

UML

UML

通过工厂方法模式的类图可以看到,工厂方法模式有四个要素:

  • 工厂接口。工厂接口是工厂方法模式的核心,与调用者直接交互用来提供产品。在实际编程中,有时候也会使用一个抽象类来作为与调用者交互的接口,其本质上是一样的。

  • 工厂实现。在编程中,工厂实现决定如何实例化产品,是实现扩展的途径,需要有多少种产品,就需要有多少个具体的工厂实现。

  • 产品接口。产品接口的主要目的是定义产品的规范,所有的产品实现都必须遵循产品接口定义的规范。产品接口是调用者最为关心的,产品接口定义的优劣直接决定了调用者代码的稳定性。同样,产品接口也可以用抽象类来代替,但要注意最好不要违反里氏替换原则。

  • 产品实现。实现产品接口的具体类,决定了产品在客户端中的具体行为。

前文提到的简单工厂模式跟工厂方法模式极为相似,区别是:简单工厂只有三个要素,他没有工厂接口,并且得到产品的方法一般是静态的。因为没有工厂接口,所以在工厂实现的扩展性方面稍弱,可以算所工厂方法模式的简化版,关于简单工厂模式,最后会简单说两句。

举例

如果抛开设计模式的范畴,“工厂方法”这个词也可以指作为“工厂”的方法,这个方法的主要目的就是创建对象,而这个方法不一定在单独的工厂类中。这些方法通常作为静态方法,定义在方法所实例化的类中。

每个工厂方法都有特定的名称。在许多面向对象的编程语言中,构造方法必须和它所在的类具有相同的名称,这样的话,如果有多种创建对象的方式(重载)就可能导致歧义。工厂方法没有这种限制,所以可以具有描述性的名称。举例来说,根据两个实数创建一个复数,而这两个实数表示直角坐标或极坐标,如果使用工厂方法,方法的含义就非常清晰了。当工厂方法起到这种消除歧义的作用时,构造方法常常被设置为私有方法,从而强制客户端代码使用工厂方法创建对象。

下面的例子展示了Java中实现复数创建的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);

适用性

下列情况可以考虑使用工厂方法模式:

  • 创建对象需要大量重复的代码。
  • 创建对象需要访问某些信息,而这些信息不应该包含在复合类中。
  • 创建对象的生命周期必须集中管理,以保证在整个程序中具有一致的行为。

工厂方法模式常见于工具包和框架中,在这些库中可能需要创建客户端代码实现的具体类型的对象。

平行的类层次结构中,常常需要一个层次结构中的对象能够根据需要创建另一个层次结构中的对象。

工厂方法模式可以用于测试驱动开发,从而允许将类放在测试中。举例来说,Foo这个类创建了一个Dangerous对象,但是Dangerous对象不能放在自动的单元测试中(可能它需要访问产品数据库,而这个数据库不是随时能够访问到的)。所以,就可以把Dangerous对象的创建交由Foo类的一个方法(虚函数)createDangerous完成。为了测试,再创建一个Foo的一个子类TestFoo,重写createDangerous方法,在方法中创建并返回一个FakeDangerousDangerous的子类),而这是一个模拟对象。这样,单元测试就可以使用TestFoo来测试Foo的功能,从而避免了使用Dangerous对象带来的副作用。

不管是简单工厂模式,工厂方法模式还是抽象工厂模式,他们具有类似的特性,所以他们的适用场景也是类似的:

  • 首先,作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
  • 其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。
  • 再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。

以上的话来自于CSDN博客的一篇文章,里面谈到了运用工厂方法模式的优越性,只通过了一个简单的组装汽车的小例子,这里例子过多故不再引用。所以强烈推荐戳一戳看一看:CSDN卡奴达摩的专栏——23种设计模式(2):工厂方法模式

局限性

使用工厂方法有三个局限,第一个与代码重构有关,另外两个与类的扩展有关。

  • 第一个局限是,重构已经存在的类会破坏客户端代码。例如,Complex类是一个标准的类,客户端使用构造方法将其实例化。可能会有很多这样的客户端代码:
    1
    Complex c = new Complex(-1, 0);

一旦Complex的编写者意识到Complex的实例化应该使用工厂方法实现,他会将Complex的构造方法设为私有。而此时使用它的构造方法的客户端代码就都失效了。

  • 第二个局限是,因为工厂方法所实例化的类具有私有的构造方法,所以这些类就不能扩展了。因为任何子类都必须调用父类的构造方法,但父类的私有构造方法是不能被子类调用的。

  • 第三个局限是,如果确实扩展了工厂方法所实例化的类(例如将构造方法设为保护的,虽然有风险但也是可行的),子类必须具有所有工厂方法的一套实现。例如,在上述Complex的例子中,如果Complex有了一个子类StrangeComplex,那么StrangeComplex必须提供属于它自己的所有工厂方法,否则

    1
    StrangeComplex.fromPolar(1, pi);

将会返回一个Complex(父类)的实例,而不是所希望的子类实例。但有些语言的反射特性可以避免这个问题。

通过修改底层编程语言,使工厂方法称为第一类的类成员,可以缓解这三个问题。

简单工厂模式(Simple Factory pattern)

普通的工厂方法模式通常伴随着对象的具体类型与工厂具体类型的一一对应,客户端代码根据需要选择合适的具体类型工厂使用。然而,这种选择可能包含复杂的逻辑。这时,可以创建一个单一的工厂类,用以包含这种选择逻辑,根据参数的不同选择实现不同的具体对象。这个工厂类不需要由每个具体产品实现一个自己的具体的工厂类,所以可以将工厂方法设置为静态方法。而且,工厂方法封装了对象的创建过程。如果创建过程非常复杂(比如依赖于配置文件或用户输入),工厂方法就非常有用了。比如,一个程序要读取图像文件。程序支持多种图像格式,每种格式都有一个对应的ImageReader类用来读取图像。程序每次读取图像时,需要基于文件信息创建合适类型的ImageReader。这个选择逻辑可以包装在一个简单工厂中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

GOF在《设计模式》一书中将工厂模式分为两类:工厂方法模式(Factory Method)与抽象工厂模式(Abstract Factory)。将简单工厂模式(Simple Factory)看为工厂方法模式的一种特例,两者归为一类。

简单来说就是建立一个工厂(一个函数或一个类方法)来制造新的对象。
就是从无到有。客户自己创建图片,然后拿来用。


工厂方法模式仿佛已经很完美的对对象的创建进行了包装,使得客户程序中仅仅处理抽象产品角色提供的接口,但使得对象的数量成倍增长。当产品种类非常多时,会出现大量的与之对应的工厂对象,这不是我们所希望的。

以上就是简单工厂模式与工厂方法模式的全部内容了,或许例子有点少,但是自己戳一下引用推荐的那两篇博客,仔细看一看,就会有更深的理解。

参考资料: