Read 'Design Patterns ---- Elements of Reusable Object-Oriented Software' Chapter III For Real Quick
第三章 创建型模式
本章涉及的模式有抽象工厂、建造者、工厂方法、原型以及单例。创建型模板的主要特点为,能够使一个系统中的对象创造、组合以及表达区分开。在创造型模板下构建的对象,往往通过继承(inheritance)来区分类的实例化,即一个对象会代理另一个对象的实例。从概念上看,这些模板都注重封装,尤其是系统使用的具体类;同时这些模式隐藏一个类的实例化过程,并将实例化的接口集成在一起。从结果上看,这类设计模式提供了相当大的弹性,特别是在哪一种类被创建,类的属主是谁,类是如何创建的这几个方面。
书中给出了一个“迷宫-房间”的例子,这一章节介绍的多数设计模式都可以用于提升或构建这一模型,但出于速度的目的掠过这一小节。同时出于速读的目的,笔者只选取较为重要的几种创建模式:抽象工厂、工厂方法、单例。
抽象工厂
目的:提供一个接口,用以创建以互相关联或独立对象构成的族群,而在这一过程中不用分别定义这些对象的实体类。
别名:Kit
动机:书中给出一个Widget的例子,即GUI中的组件。设想一个用户接口支持多种标准、具体的组件可以拥有不一样的外观(例如滚动条、窗口和按钮)以及行为。出于对可移植性的考虑,Widget的外观或行为应当被标准化,程序员不应该重复地在每一个Widget类中写入这些外观或行为的实现,否则在修改这些标准时,维护人员需要劳神费时地重写每一个具体类。如果我们通过抽象工厂模板解决这一问题,UML示意图如下:
图中,WidgetFactory是一个用来创建基本Widget组件的接口。同时每一种组件都有相应的抽象类(如Windows、ScrollBar),抽象类的子类(如PMWindow、PMScrollBar等)作为实体类定义有不同外观以及行为的组件。工厂接口可以返回一个新的Widget对象,但用户并不会注意到这一过程中使用了哪些具体对类或对象。WidgetFactory的子类,图中MotifyWidgetFactory以及PMWidgetFactory代表了两种不同的Widget行为或外观标准(或者说定义了两个族群),如若有一天需要改变或者更新标准时,需要编辑的类就从所有实例类缩小到了制定规范的工厂接口。
值得一提的是,这种方案加强了各个实体类之间的依赖性和关联性,用户可以通过修改工厂自行定义这些特性。
适用场景:
- 一个不依赖于产品创建、组合、表达细节的系统。
- 系统中有多以一个的族群,同时每次只能设定其中一个族群的标准。
- 一个族群中相关的产品对象应当按照”需要被一起使用“的标准来设计,使用这一模板时应当致力于强化这一限制。
- 设计者在提供一个产品类库时,只提供产品类的接口而非产品的定义和实现过程。
结构:
基本原料:
- 抽象工厂接口:定义创建抽象产品的方法。
- 实体工厂类:实现创建实体产品的方法。
- 抽象产品接口:用于创建一种产品对象。
- 实体产品类:根据具体工厂给出的标准定义一个产品对象,实现抽象产品接口。
- 客户类:仅使用模板中抽象工厂接口或抽象产品接口。
协作性:通常 一个单独的具体工厂实例在程序运行时产生,这个具体工厂创建的产品对象都具有特定的标准与实现。为了创建不同的产品对象,客户类需要使用不同的具体工厂。抽象工厂将产品对象的创建过程推迟到具体工厂的子类中。
结果:
- 这一模板分离了实例类。一个抽象工厂将产品对象的创建过程封装起来,以此来隔离客户类与实体产品类。客户类往往只能操作抽象接口。产品类的名字与实体工厂的实现相关,但这些都不会表现在客户端的代码中。
- 这一模板让产品族群交换变得简单。首先,一个具体工厂类只在程序中出现一次,因此改变一个程序使用的实体工厂非常简单。因为一个抽象工厂创造了一个完整的产品族群,因此在一个程序中,产品的族群可以轻易被改变。
- 这一模板强化了产品的一致性。当一些产品对象需要一起作用时,那么程序需要确保一次只使用来自一个族群的对象,抽象工厂类让强化的过程变得更简单。
- 支持新的产品种类变得困难。扩展抽象工厂来产生全新种类的产品类并不时那么容易,因为抽象产品接口往往数量庞大,要在每一种抽象产品下添加新的实体产品耗时费力;同时我们还需要扩展工厂接口,在抽象接口及其所有子类中加入新的方法。
工厂方法
目的:定义一个接口,通过该接口创建对象,但是对象的实例化由子类决定。工厂方法让一个类的实例化延时到子类中。
别名:虚拟构造器
动机:框架往往使用抽象类定义以及维护对象之间的依赖关系。同时,一个框架往往需要负责对象的创造。思考一个情景,一个应用的框架可以向用户表达多个不同的文档,那么,在这个框架中,有两个关键的抽象类,一是应用类,二是文档类。这样一来,客户类不得不成为这两个类的子类,由此才能得到这两个抽象类的实现。
引用书中的例子,在一个绘图软件中,我们定义了一个DrawingApplication类以及DrawingDocument类,即前文的两个关键抽象类。Application类用于管理Document类,并且按照需求创造Document对象,比如用户在Application界面里选择‘New’选项。因为特定的Document子类的实例化由软件决定,所以Application类不能预估Document的子类如何实例化,Application类通常只知道什么样的Document类应该被创建。这就让开发人员陷入两难,框架一定要实例化一些类,但是通过抽象类无法完成实例化。
我们使用工厂方法来解决这一问题。以下为UML图解:
这一模式封装Document类的信息,并将封装好的对象传送出框架。Application的子类会重定义一个抽象的CreateDocument()方法,这个方法返回Document的子类。一旦一个Application的子类完成实例化,它可以实例化一个个性化的子类。
适用情况:
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类需要它的子类来定义它所创建的对象。
-
将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
结构:
基本原料:
- 产品接口:定义用于创建工厂方法的接口。
- 实体产品类:实现产品接口。
- 创造者接口:定义工厂方法,返回一个指定类别的产品对象。创造者接口中可能会包含实体产品类的默认实现。创造者接口也有可能调用工厂方法来创造产品类。
- 实体创造者类:继承并重写工厂方法,返回实体产品类的实例。
协作性:创造者仰赖于其子类来定义工厂方法,以此来创建一个实体工厂类的实例。
结果:使用工厂模板,开发人员不必再将软件定义类与代码捆绑在一起,即,开发人员只用写作产品模板,而系统通过自定义的实体产品类就能产出产品实例。这样做的缺点也十分显著,客户端可以有权访问创造者类的子类,并以此创造产品实例。然而根据开发逻辑,只有在客户端不得不子类化创造者类时,这样做才是可以接受的。
- 向子类提供钩子。在工厂方法中创造一个对象比直接创造一个对象更具有弹性。工厂方法给与子类钩子,通过钩子,开发人员可以完成对子类的升级与维护。
- 连接平行类的分层。在大部分的工厂方法模式中,客户端只调用到Creator类或接口。但不仅于此,客户端可以将这个模板作用于平行类的分层。
单例
目的:确保一个类只有一个实例,并且提供一个全局指针用以访问该实例。
动机:在一些情境中,我们希望一些类只有一个实例。例如一个软件中的文件管理系统或者窗口管理系统。我们通过全局变量来实现这一目标,然而我们依然能多次实例化这个类。还有更优的解决方案,让类本身负责检查它的唯一实例,即单例模式。
适用情况:
- 一个类只能有一个实例,这个实例能被客户类访问。
- 实例可以被子类拓展,客户类可以使用拓展的实例而不用修改父类代码。
基本原料:
- 单例类:定义一个实例操作方法,客户类可以借此访问这个类的唯一实例。这个类负责创建自己的唯一实例。
协作性:客户类可以通过单例类中的实例操作方法访问该类的唯一实例。
结果:
- 访问单例的权限得到控制。
- 减少存储对象名使用的空间。
- 允许通过继承重定义其表现以及操作方法。
- 允许可变数目的实例存在,因为单例类只能通过其规定的操作方法访问。
- 比起类操作有更多的弹性。
关于创造型模式的思考与讨论
我们可以使用两种方法来参数化一个系统,一是创造子类,我们可以运用工厂方法模式。这个方法的主要缺点是创造一个新的产品类时,我们需要在模式中添加一个新的子类。另一种方法更加依赖对象的组合,定义一个对象,该对象了解所有的产品类,并将该对象作为一个参数置于系统中。这种方法采取了上文中抽象工厂模式的思路。
总的来说,在制作一个绘画软件框架时最好使用原型模式,因为在每一个不同的图像类中写入clone()方法就能实现这一系统,同时这样能够显著减少类的数量。clone()方法可以适用更多的场景,而不仅仅是单纯的实例化。
工厂方法只增加了一点点复杂度,就能使得设计出的产品更加个性化。别的设计模式往往需要新的实体产品类,而工厂方法只需要新的操作方法。
使用抽象工厂设计的软件更具有弹性(相较于工厂方法而言),但这些程序更加复杂。通常来说,一开始使用工厂方法模式的程序会逐渐转变成别的创建型模式,因为开发人员需要更多的弹性。所以,了解更多的设计模式给与我们更多选择,我们能权衡每一种设计模式的利弊做出最好的选择。