设计模式 学习笔记 之十

第13章 Factory模式族

13.1 为什么需要Factory?

在前面的几章中,我们已经学习了几种常用的设计模式。我们不难发现,设计模式的一个核心思想就是:努力实现“依赖抽象原则”,即客户代码应该依赖于抽象接口编程,而不是依赖于具体实现类编程。但是,在学习这些设计模式的过程中,我们也有意忽略一个必须解决的问题:抽象接口是无法实例化的,只有具体实现类才能被实例化,因此客户代码虽然依赖于抽象接口,但它真正使用的还是具体实现类的对象。因此,客户代码还是需要去创建出这些具体实现类的对象,系统才能运行起来。这样,由于客户代码还是需要直接去实例化具体实现类的对象,因此这就要求客户代码要了解实现细节,从而违背了依赖抽象原则。

clip_image002

怎样才能让客户代码真正做到仅依赖于抽象?让我们先分析一下客户代码无法仅依赖于抽象的原因。在这里,客户代码实际上承担了两项职责:一是使用抽象接口来完成任务,由于这是基于抽象接口的,客户代码无需了解具体实现类的细节;二是创建和管理具体实现类的对象,这是直接与实现类打交道,因此不是基于抽象接口编程。由此可见,客户代码把两个职责(“使用对象”和“创建/管理对象”)耦合在一起了,这导致它无法真正地仅依赖于抽象。

找到了问题的症结所在,那么我们来看一下怎么解决它。其实答案在学习单一职责原则时就已经讲到了:拆分百耦合的职责,让不同的class各司其职。因此,我们的办法就是把对象的使用与对象的创建/管理分离开。客户代码仅仅是对象的使用者,只要用好对象就行了;而对象的创建和管理则交给另一个类,这个新引入的类就是所谓的“Factory”。这样,新的设计变为下面的样子。

clip_image004

这个设计是一个更好的设计吗?我们来分析一下。

首先,从耦合的角度来看,客户代码与Factory现在都是单一职责。由于它们各司其职,因此互不影响,从而是松散耦合的。

其次,从扩展性的角度看,当有新的具体实现类引入时,由于现在客户代码完全依赖于抽象,所以客户代码无需作变动,而只需扩展Factory并加入新的具体实现类。因此我们无需修改已有代码就能扩展功能,因此满足了开放封闭原则。

最后,由于把原先客户代码中耦合的职责拆分开来,客户代码只负责使用对象,而Factory负责对象的创建与管理,因此复杂的逻辑被封装到不同的模块,这样系统的复杂性也降低了。

这也就回答了本节标题所提出的问题:为什么要引入Factory?通过引入Factory,对象创建与管理的逻辑被封装到Factory中,从而保证了客户代码完全仅依赖于抽象,系统的复杂性降低,耦合度减弱,更易于扩展。

在实际的软件设计中,我们可以按照下面的两个步骤来操作:

  • 首先,使用恰当的设计模式来解决手头的问题。在这个过程中,不要去担心具体实现类的对象的创建和管理问题。把注意力集中在找到抽象接口及客户代码应如何依赖于抽象编程这个问题上。
  • 然后,当真正需要关注对象的创建和管理时,引入Factory,在Factory中封装对象的创建与管理的规则。

Factory的使用实际上也是有规律可循的,因此前人也总结了Factory的模式。在接下来的几个小节中,我们将学习这些模式。

13.2 Simple Factory模式

本节中我们来学习最常用到的一种Factory模式:Simple Factory模式。在这个模式中,充当Factory的是一个具体类,它根据若干信息(如传入参数,或配置文件)来创建恰当的对象。我们来看一个例子。

例子:TaxCalculatorFactory

前面在学习Strategy模式时,我们举了税费计算的例子。在那个例子中,我们最后得到了下面的设计图。

clip_image006

可以看出,SalesOrder属于客户代码,而TaxCalculator是它所依赖的抽象接口。根据我们前一节所述内容,为了让SalesOrder真正仅依赖于TaxCalculator抽象,我们应该给TaxCalculator引入一个Factory,在这个Factory中封装TaxCalculator子类的创建逻辑。这里我们用一个名为TaxCalculatorFactory的具体类来充当这个Factory,这样就得到下面的设计。

clip_image008

这就是Simple Factory模式。这里要注意以下几点:

  • 始终记住引入Factory的根本目的,是让客户代码真正地仅依赖于抽象。为了做到这一点,Factory的方法所返回的类型也只能是抽象接口,而不能是具体实现类。在这个例子中,Factory就只能返回TaxCalculator,而绝不能返回USTaxCalculator类型或ChinaTaxCalculator类型。
  • TaxCalculatorFactory作为TaxCalculator class hierarchy的Factory,需要负责TaxCalculator class hierarchy中每个具体实现类的创建和管理细节。当class hierarchy中引入了新的具体实现类时,Factory也要作相应的扩展。由此可见,我们做到了让客户代码真正仅依赖于抽象,但付出的代价是让Factory与抽象接口的class hierarchy产生紧密耦合。
  • Simple Factory模式中,Factory由一个具体类充当。如果这个具体类仅仅具有创建和管理对象的职责,那么通常情况下它应该最多只有一个实例。因此,这个Factory类应该建模成一个Singleton类,甚至我们可以更进一点,强制让这个Factory类不能进行实例化而退化成一个utility class。

Simple Factory模式在实现上有一些变种。下面我们来看一下。

变种1:一个类既有首要职责,同时又充当一个class hierarchySimple Factory

在Java标准库中,有一个URL类,它就同时承担两种职责:首要职责是表示一个URL,但同时它也是URLConnection这个class hierarchy的Simple Factory。我们可以把这个情况用下图表示。

clip_image010

变种2:把Factory与抽象接口合并

在Java标准库,DateFormat是一个抽象类,它是不能实例化的,其作用是定义抽象接口。目前它有一个具体实现类SimpleDateFormat。Java语言并没有为DateFormat专门配置一个Factory,它的作法是让DateFormat抽象类自己充当自己所在class hierarchy的Factory。在DateFormat中定义了若干静态方法,这些静态方法就是用于创建DateFormat的具体实现类SimpleDateFormat的对象的。我们用下面的UML图来表示。

clip_image012

小结

Simple Factory模式是一种最常用的Factory模式。通过引入一个Factory,我们可以使客户代码真正做到仅依赖于抽象。而在具体实现上,Factory可能是一个独立的类,也可能是被耦合到一个具有其它职责的类中,还可能被合并到抽象接口中。但是,不管实现上怎么变化,最关键的一个识别根据是:Factory方法的返回类型必然是抽象接口,而绝不会是一个具体实现类,因为只有这样,它才能做到让客户代码真正仅依赖于抽象。

13.3 Abstract Factory模式

有时候,若干个class需要以一种协调一致的方式被实例化。最常见的例子是GUI控件的实例化。当工作在Windows平台时,系统需要实例化Windows的按钮、单选钮、列表框、文本框等GUI控件,而当工作在Unix平台时,系统又需要实例化Unix的按钮、单选钮、列表框、文本框等控件。这种需要以协调一致的方式被实例化的类往往组成一个“类家族”(class family)。比如在刚才讲的例子中,按钮、单选钮、列表框、文本框这些控件一起构成了“GUI控件类家族”,而且存在针对Windows平台的“Windows GUI控件类家族”和针对Unix平台的“Unix GUI控件类家族”。

当存在这种以协调一致的方式实例化“类家族”的需求时,Abstract Factory模式是最适用的模式。我们下面就以“GUI控件类家族”为例来学习这个模式。我们现在的问题是:需要让软件基于所运行的操作系统平台(Windows或Unix)来实例化相应的GUI控件。

对待这个问题,之前讲到的共性变性分析同样是适用的。我们不难看出,在这个问题中,GUI控件类家族就是一个共性,而对应的变性则是存在Windows GUI控件类家族和Unix GUI控件类家族。我们用下面的图来表示这个共性变性分析的结果。

clip_image002[1]

但分析到这一步只是完成了任务的一半,我们还需要能够依据不同的操作平台来实例化不同的GUI控件类家族。因此我们还需要再作一个共性变性分析。在这第2个共性变性分析中,我们看到,共性是要引入Factory来完成GUI控件类家族的实例化,变性则是能实例化出不同的GUI控件类家族。基于这个共性变性分析,我们得到下面的设计。

clip_image004[1]

综合这两个共性变性分析,我们得到最终的设计如下。

clip_image006[1]

这个最终的设计实际上就是Abstract Factory模式。我们注意到在这个模式应用时,要做两次共性变性分析。但是,识别出这个模式的应用场合的最好办法还是识别出“类家族”。只要存在由若干类组成一个类家族的情况,那么对类家族的创建问题基本上都应该是通过Abstract Factory模式来完成。

类家族常常依据以下的条件来定义:

  • 不同的操作系统
  • 不同的性能指标
  • 不同的程序版本
  • 不同的用户特点
  • 不同的地域特点

要注意的是,由于在Abstract Factory模式中,具体的Factory类也被隐藏在一个Factory抽象接口的背后,因此客户代码必须有一个办法来获取具体的Factory类。这就要求为Factory抽象接口再引入一个Simple Factory。

小结

在软件开发中,有时会出现“类家族”的现象,它们可能是以操作系统、性能指标、程序版本、用户特点或地域特点等条件来定义的。如果需要对类家族进行实例化,就应该使用Abstract Factory模式。

13.4 Factory Method模式

与前面已经学习的两种Factory模式相比较而言,Factory Method模式的应用并不是太多。Factory Method模式最常见的应用是为一个class hierarchy创建一个平行的class hierarchy,这个平行的class hierarchy被赋予某些委派的职责。Java Collection框架中的迭代器也许是讲述Factory Method模式最好的例子。

Java Collection框架定义了Collection抽象接口,这个接口为常用的数据结构定义了行为。另一方面,Java Collection框架也定义了Iterator接口,这个接口为迭代器定义了行为。一种数据结构实际上是对应了一种迭代器的,因此Collection class hierarchy与Iterator class hierarchy是平行的class hierarchy。Java Collection框架在Collection接口中定义了iterator()方法,这个方法就把这两个平行的class hierarchy关联起来了。下面是它的设计原理图。

clip_image008[1]

posted @ 2011-05-10 11:17  李嘉 (Justin)  阅读(228)  评论(0编辑  收藏  举报