Skip to content

创建型模式

引言

创建型设计模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。

在这些模式中有两个不断出现的主旋律:

  • 第一,它们都将关于该系统使用哪些具体的类的信息封装起来。
  • 第二,它们隐藏了这些类的实例是如何被创建和放在一起的。

整个系统关于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在什么被创建、谁创建它、它是怎样被创建的,以及何时创建等方面给予你很大的灵活性。它们允许你用结构和功能差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时指定)。

Abstract Factory(抽象工厂)——对象创建型模式

意图

提供一个接口以创建一系列相关或相互依赖的对象,而无须指定它们具体的类。

别名

Kit

动机

定义一个抽象类,为创建每一种不同的基本组件提供接口,客户调用抽象类的操作获取实例,却不需要知道使用的是哪些具体类。

适用性

在以下情况下使用 Abstract Factory 模式:

  • 一个系统要独立于它的产品的创建、组合和表示。
  • 一个系统要由多个产品系列中的一个来配置。
  • 要强调一系列相关的产品对象的设计以便进行联合使用。
  • 提供一个产品类库,但只想要它的接口而不是实现。

参与者

  • AbstractFactory: 声明一个创建抽象产品对象的操作接口。
  • ConcreteFactory: 实现创建具体产品对象的操作。
  • AbstractProduct: 为一类产品对象声明一个接口。
  • ConcreteProduct: 定义一个将被相应的具体工厂创建的产品对象。实现 AbstractProduct 接口。
  • Client: 仅使用由 AbstractFactory 和 AbstractProduct 类声明的接口。

协作

  • 通常在运行时创建一个 ConcreteFactory 类的实例。这一具体的工厂创建具有特定实现的产品对象。为创建不同的产品对象,客户应使用不同的具体工厂。
  • AbstractFactory 将产品对象的创建延迟到它的 ConcreteFactory 子类。

效果

  • 分离了具体的类:  AbstractFactory 模式帮助你控制一个应用创建的对象的类。因为一个工厂封装创建产品对象的责任和过程,它将客户与类的实现分离。客户通过它们的抽象接口操纵实例。产品的类名也在具体工厂的实现中被隔离,即它们不出现在客户代码中。
  • 使得易于交换产品系列: 一个具体工厂类在一个应用中仅出现一次——在它初始化的时候。这使得改变一个应用的具体工厂变得很容易。只需要改变具体的工厂即可使用不同的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻改变。
  • 有利于产品的一致性: 当一个系列中的产品对象被设计成一起工作时,一个应用一次只能使用同一个系列中的对象,这一点很重要。而 AbstractFactory 很容易实现这一点。
  • 难以支持新种类的产品 难以扩展抽象工厂以生产新种类的产品。这是因为 AbstractFactory 接口确定了可以被创建的产品集合。支持新种类的产品就需要扩展该工厂接口,这将涉及 AbstractFactory 类及其所有子类的改变。

实现

  1. 将工厂作为单件 一个应用中一般每个产品系列只需要一个 ConcreteFactory 的实例。因此工厂通常最好实现为一个 Singleton
  2. 创建产品 AbstractFactory 仅声明一个创建产品的接口,真正创建产品是由 ConcreteProduct 子类实现的。最通常的办法是为每一个产品定义一个工厂方法。一个具体的工厂将为每个产品重定义该工厂方法以指定产品。虽然这样的实现很简单,但它却要求每个产品系列都要有一个新的具体工厂子类,即使这些产品系列的差别很小。
  3. 定义可扩展的工厂
    • AbstractFactory 通常为每一种它可以生产的产品定义一个操作。产品的种类被编码在操作型构(signature)中。增加一种新的产品要求改变 AbstractFactory 的接口以及所有与它相关的类。
    • 一个更灵活但不太安全的设计是给创建对象的操作增加一个参数。该参数指定了将被创建的对象的种类。它可以是一个类标识符、一个整数、一个字符串,或其他任何可以标识这种产品的东西。实际上,使用这种方法 AbstractFactory 只需要一个“Make”操作和一个指示要创建对象的种类的参数。

相关模式

  • AbstractFactory 类通常用工厂方法(Factory Method(3.3))实现,但它们也可以用 Prototype 实现。
  • 一个具体的工厂通常是一个单件(Singleton(3.5))。

Builder(生成器)——对象创建型模式

意图

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

动机

通过**导向器(director)分析并传递标志给生成器(builder)**来选择合适的子类从而生成合适的复杂对象。

例如在文本阅读器中,我们先通过文本分析器(导向器)进行语法分析,然后将标志传给转换器(生成器)来选择合适的子类如 ASCllConvert 或 TextConvert 生成一个复杂对象(或算法)如 ASCll 文本或者交互式文本窗口组件等。

适用性

  • 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
  • 当构造过程必须允许被构造的对象有不同的表示时。

参与者

  • Builder: —为创建一个 Product 对象的各个部件指定抽象接口。
  • ConcreteBuilder
    • 实现 Builder 的接口以构造和装配该产品的各个部件。
    • 定义并跟踪它所创建的表示。
    • 提供一个检索产品的接口(例如,GetASCIIText 和 GetTextWidget)。
  • Director: 构造一个使用 Builder 接口的对象。
  • Product
    • 表示被构造的复杂对象。ConcreteBuilder 创建该产品的内部表示并定义它的装配过程。
    • 包含定义组成部件的类,包括将这些部件装配成最终产品的接口

协作

  1. 客户创建 Director 对象,并用它所想要的 Builder 对象进行配置。
  2. 一旦生成了产品部件,导向器就会通知生成器。
  3. 生成器处理导向器的请求,并将部件添加到该产品中。
  4. 客户从生成器中检索产品。

效果

  1. 改变一个产品的内部表示: Builder 对象提供给导向器一个构造产品的抽象接口。该接口使得生成器可以隐藏这个产品的表示和内部结构。它同时也隐藏了该产品是如何装配的。因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定义一个新的生成器。
  2. 将构造代码和表示代码分开: Builder 模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。客户不需要知道定义产品内部结构的类的所有信息,这些类是不出现在 Builder 接口中的。每个 ConcreteBuilder 包含了创建和装配一个特定产品的所有代码。这些代码只需要写一次;然后不同的 Director 可以复用它以在相同部件集合的基础上构建不同的 Product。
  3. 对构造过程更加精细的控制: Builder 模式与一下子就生成产品的创建型模式不同,它是在导向器的控制下一步一步构造产品的。仅当该产品完成时导向器才从生成器中取回它。因此 Builder 接口相比其他创建型模式能更好地反映产品的构造过程。这使你可以更精细地控制构建过程,从而能更精细地控制所得产品的内部结构。

实现

通常有一个抽象的 Builder 类为导向器可能要求创建的每一个构件定义一个操作。这些操作缺省情况下什么都不做。一个 ConcreteBuilder 类对它有兴趣创建的构件重定义这些操作。

以下是一些需要考虑的问题

  1. 装配和构造接口: 生成器逐步地构造它们的产品。因此 Builder 类接口必须足够普遍,以便为各种类型的具体生成器构造产品。
  2. 为什么产品没有抽象类: 通常情况下,由具体生成器生成的产品,其表示相差非常大,以至于给不同的产品以公共父类没有太大意思。
  3. 在 Builder 中缺省的方法为空

相关模式

  • Abstract Factory(3.1)与 Builder 相似,因为它也可以创建复杂对象。主要的区别是 Builder 模式着重于一步步构造一个复杂对象。而 Abstract Factory 着重于多个系列的产品对象(简单的或是复杂的)。Builder 在最后一步返回产品,而对于 Abstract Factory 来说,产品是立即返回的。
  • Composite(4.3)通常是用 Builder 生成的。

Factory Method(工厂方法)——对象创建型模式

意图

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。

别名

虚构造器(virtual constructor)。

动机

框架使用抽象类定义和维护对象之间的关系。这些对象的创建通常也由框架负责。即封装一个类被创建的信息并将信息从框架中分离出来,框架提供一个方法专门负责生产这个类的实例。

适用性

  • 当一个类不知道它所必须创建的对象的类的时候。
  • 当一个类希望由它的子类来指定它所创建的对象的时候。
  • 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。

参与者

  • Product: 定义工厂方法所创建的对象。
  • ConcreteProduct: 实现创建 Product 接口。
  • Creator
    • 声明工厂方法,该方法返回一个 Product 类型的对象。
    • Creator 也可以定义一个工厂方法的缺省实现,它返回一个缺省的 ConcreteProduct 对象。
    • 可以调用工厂方法以创建一个 Product 对象。
  • ConcreteCreator: 重定义工厂方法以返回一个 ConcreteProduct 实例。

协作

Creator 依赖于它的子类来定义工厂方法,所以它返回一个适当的 ConcreteProduct 实例。

效果

工厂方法不再将与特定应用有关的类绑定到你的代码中。代码仅处理 Product 接口,因此它可以与用户定义的任何 ConcreteProduct 类一起使用。

工厂方法的一个潜在缺点在于,客户可能仅仅为了创建一个特定的 ConcreteProduct 对象,就不得不创建 Creator 的子类。当 Creator 子类不是必需的时,客户现在必然要处理类演化的其他方面。但是当客户无论如何必须创建 Creator 的子类时,创建子类也是可行的。

下面是 Factory Method 模式的另外两种效果:

  1. 为子类提供钩子(hook): 用工厂方法在一个类的内部创建对象通常比直接创建对象更灵活。Factory Method 给子类一个钩子以提供对象的扩展版本。
  2. 连接平行的类层次: 迄今为止,在我们所考虑的例子中,工厂方法并不只是被 Creator 调用,客户可以找到一些有用的工厂方法,尤其在平行的类层次的情况下。当一个类将它的一些职责委托给一个独立的类的时候,就产生了平行类层次。

实现

当应用 Factory Method 模式时要考虑下面一些问题:

  1. 主要有两种不同的情况
    1. Creator 类是一个抽象类并且不提供它所声明的工厂方法的实现。
      • 需要子类来定义实现,因为没有合理的缺省实现。它避免了不得不实例化不可预见类的问题;
    2. Creator 是一个具体的类而且为工厂方法提供一个缺省的实现。也有可能有一个定义了缺省实现的抽象类,但这不太常见。
      • 具体的 Creator 主要由于灵活性才使用工厂方法。它所遵循的准则是,“用一个独立的操作创建对象,这样子类才能重定义它们的创建方式。”这条准则保证了子类的设计者能够在必要的时候改变父类所实例化的对象的类。
  2. 参数化工厂方法: 该模式的另一种情况使得工厂方法可以创建多种产品。工厂方法采用一个标识要被创建的对象种类的参数。工厂方法创建的所有对象将共享 Product 接口。重定义一个参数化的工厂方法使你可以简单而有选择性地扩展或改变一个 Creator 生产的产品。你可以为新产品引入新的标识符,或将已有的标识符与不同的产品相关联。

相关模式

  • Abstract Factory(3.1)经常用工厂方法来实现。Abstract Factory 模式中动机一节的例子也对 Factory Method 进行了说明。
  • 工厂方法通常在 Template Method(5.10)中被调用。
  • Prototype(3.4)不需要创建 Creator 的子类。但是,它们通常要求一个针对 Product 类的 Initialize 操作 Creator 使用 Initialize 来初始化对象,而 Factory Method 不需要这样的操作。

Prototype(原型)——对象创建型模式

意图

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

动机

通过拷贝或者克隆一个实例来创建新的对象,我们称这个实例为原型,从而实现减少类的数目,也更易于增加新的类目。

例如,我们有一个 GraphicTool 的类负责创建音乐类的实例并把它们添加到乐谱中,我们可以为每一种音乐对象创建一个 GraphicTool 的子类,但是这样会产生大量的子类,但他们仅在初始化的音乐对象类别有所不同。我们知道对象组合是比创建子类更灵活的一种选择。问题是,该框架怎样用它来参数化 GraphicTool 的实例,而这些实例是由 Graphic 类所支持创建的,这就用到了 原型

适用性

  • 当一个系统应该独立于它的产品创建、构成和表示时。
  • 当要实例化的类是在运行时指定时,例如,通过动态装载。
  • 为了避免创建一个与产品类层次平行的工厂类层次时。
  • 当一个类的实例只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

参与者

  • Prototype: 声明一个克隆自身的接口。
  • ConcretePrototype: 实现一个克隆自身的操作。
  • Client: 让一个原型克隆自身从而创建一个新的对象。

协作

客户请求一个原型克隆自身。

效果

Prototype 有许多与 Abstract Factory(3.1)和 Builder(3.2)一样的效果:它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目。此外,这些模式使客户无须改变即可使用与特定应用相关的类。

下面列出 Prototype 模式的另外一些优点。

  • 运行时增加和删除产品:  Prototype 允许只通过客户注册原型实例就将一个新的具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时建立和删除原型。
  • 改变值以指定新对象: 高度动态的系统允许你通过对象组合定义新的行为——例如,通过为一个对象变量指定值——并且不定义新的类。你通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。
    • 这种设计使得用户无须编程即可定义新“类”。实际上,克隆一个原型类似于实例化一个类。Prototype 模式可以极大地减少系统所需要的类的数目。在我们的音乐编辑器中,一个 GraphicTool 类可以创建无数种音乐对象。
  • 改变结构以指定新对象: 许多应用由部件和子部件来创建对象。例如电路设计编辑器就是由子电路来构造电路的。为方便起见,这样的应用通常允许你实例化复杂的、用户定义的结构,比方说,一次又一次地重复使用一个特定的子电路。
    • Prototype 模式也支持这一点。我们仅需将这个子电路作为一个原型增加到可用的电路元素选择板中。只要组合电路对象将 Clone 实现为一个深拷贝(deep copy),具有不同结构的电路就可以是原型了。
  • 减少子类的构造:  Factory Method(3.3)经常产生一个与产品类层次平行的 Creator 类层次。Prototype 模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象,因此你根本不需要 Creator 类层次。
  • 用类动态配置应用 一些运行时环境允许你动态地将类装载到应用中。
    • 一个希望创建动态载入类的实例的应用不能静态引用类的构造器,而应该由运行环境在载入时自动创建每个类的实例,并用原型管理器来注册这个实例。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。

Prototype 的主要缺陷是每一个 Prototype 的子类都必须实现 Clone 操作,这可能很困难。例如,当所考虑的类已经存在时就难以新增 Clone 操作。当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难。

实现

因为在像 C++这样的静态语言中,类不是对象,并且运行时只能得到很少或者得不到任何类型信息,所以 Prototype 特别有用。而在 Smalltalk 或 Objective C 这样的语言中 Prototype 就不是那么重要了,因为这些语言提供了一个等价于原型的东西(即类对象)来创建每个类的实例。Prototype 模式在像 Self 这样基于原型的语言中是固有的,所有对象的创建都是通过克隆一个原型实现的。

当实现原型时,要考虑下面一些问题:

使用一个原型管理器

当一个系统中原型数目不固定时(也就是说,它们可以动态创建和销毁),要保持一个可用原型的注册表。客户不会自己来管理原型,但会在注册表中存储和检索原型。客户在克隆一个原型前会向注册表请求该原型。我们称这个注册表为原型管理器(prototype manager)。

原型管理器是一个关联存储器(associative store),它返回一个与给定关键字相匹配的原型。它有一些操作可以用来通过关键字注册原型和解除注册。客户可以在运行时更改甚至浏览这个注册表。这使得客户无须编写代码就可以扩展并得到系统清单。

实现克隆操作

Prototype 模式最困难的部分在于正确实现 Clone 操作。当对象结构包含循环引用时,这尤为棘手。

大多数语言都对克隆对象提供了一些支持。例如,Smalltalk 提供了一个 copy 的实现,它被所有 Object 的子类所继承。C++提供了一个拷贝构造器。但这些工具并不能解决“浅拷贝和深拷贝”问题。也就是说,克隆一个对象是依次克隆它的实例变量,还是由克隆对象和原对象共享这些变量?

浅拷贝简单并且通常足够了这意味着在拷贝的对象和原来的对象之间是共享指针的。但克隆一个结构复杂的原型通常需要深拷贝,因为复制对象和原对象必须相互独立。因此你必须保证克隆对象的构件也是对原型的构件的克隆。克隆迫使你决定如果所有东西都被共享了该怎么办。

如果系统中的对象提供了 Save 和 Load 操作,那么你只需通过保存对象和立刻载入对象,就可以为 Clone 操作提供一个缺省实现。Save 操作将该对象保存在内存缓冲区中,而 Load 则通过从该缓冲区中重构这个对象来创建一个副本。

初始化克隆对象

当一些客户对克隆对象已经相当满意时,另一些客户将会希望使用他们所选择的一些值来初始化该对象的一些或是所有的内部状态。一般来说不可能在 Clone 操作中传递这些值,因为这些值的数目会由于原型的类的不同而有所不同。一些原型可能需要多个初始化参数,另一些可能什么也不要。在 Clone 操作中传递参数会破坏克隆接口的统一性。

可能会出现这样的情况,即原型的类已经为(重)设定一些关键的状态值定义好了操作。如果这样的话,客户在克隆后马上就可以使用这些操作。否则,你就可能不得不引入一个 Initialize 操作,该操作使用初始化参数并据此设定克隆对象的内部状态。注意深拷贝 Clone 操作——一些副本在你重新初始化它们之前可能必须要删除掉(删除可以显式地做也可以在 Initialize 内部做)。

相关模式

  • 正如我们在这一章结尾所讨论的那样,Prototype 和 Abstract Factory(3.1)模式在某些方面是相互竞争的。但是它们也可以一起使用。Abstract Factory 可以存储一个被克隆的原型的集合,并且返回产品对象。
  • 大量使用 Composite(4.3)和 Decorator(4.4)模式的设计通常也可从 Prototype 模式获益。

Singleton(单件)——对象创建型模式

意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

动机

对一些类来说,怎样才能保证一个类只有一个实例并且这个实例易于被访问呢?全局变量使得一个对象可以被访问,但它不能防止你实例化多个对象。一个更好的办法是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法

适用性

在下面的情况下可以使用 Singleton 模式:

  • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
  • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无须更改代码就能使用一个扩展的实例时。

参与者

  • Singleton
    • 定义一个 Instance 操作,允许客户访问它的唯一实例。Instance 是一个类操作(即 Smalltalk 中的一个类方法和 C++中的一个静态成员函数)。
    • 可能负责创建它自己的唯一实例。

协作

客户只能通过 Singleton 的 Instance 操作访问一个 Singleton 的实例。

效果

  • 对唯一实例的受控访问: 因为 Singleton 类封装它的唯一实例,所以它可以严格地控制客户怎样以及何时访问它。
  • 缩小名字空间:  Singleton 模式是对全局变量的一种改进,它避免了那些存储唯一实例的全局变量污染名字空间。
  • 允许对操作和表示的精化:  Singleton 类可以有子类,而且用这个扩展类的实例来配置一个应用是很容易的。你可以用你所需要的类的实例在运行时配置应用。
  • 允许可变数目的实例: 这个模式使得你易于改变你的想法,并允许 Singleton 类的多个实例。此外,你可以用相同的方法来控制应用所使用的实例的数目。只有允许访问 Singleton 实例的操作需要改变。
  • 比类操作更灵活: 另一种封装单件功能的方式是使用类操作(即 C++中的静态成员函数或者是 Smalltalk 中的类方法)。但这两种语言技术都难以改变设计以允许一个类有多个实例。此外,C++中的静态成员函数不是虚函数,因此子类不能多态地重定义它们。

实现

  • 保证一个唯一的实例: Singleton 模式使得这个唯一实例是类的一般实例,但该类被写成只有一个实例能被创建。做到这一点的一个常用方法是将创建这个实例的操作隐藏在一个类操作(即一个静态成员函数或者是一个类方法)后面,由它保证只有一个实例被创建。这个操作可以访问保存唯一实例的变量,而且它可以保证这个变量在返回值之前用这个唯一实例初始化。这种方法保证了单件在它的首次使用前被创建和使用。
  • 创建 Singleton 类的子类: 主要问题与其说是定义子类不如说是建立它的唯一实例,这样客户就可以使用它。事实上,指向单件实例的变量必须用子类的实例进行初始化。
    • 最简单的技术是在 Singleton 的 Instance 操作中决定你想使用的是哪一个单件。
    • 另一个选择 Singleton 的子类的方法是将 Instance 的实现从父类(即 MazeFactory)中分离出来并将它放入子类。
    • 链接的方法在链接时确定了单件类的选择,这使得难以在运行时选择单件类。使用条件语句来决定子类更加灵活一些,但这硬性限定(hard-wire)了可能的 Singleton 类的集合。“这两种方法不是在所有的情况都足够灵活。
    • 一个更灵活的方法是使用一个单件注册表(registry of singleton)。可能的 Singleton 类的集合不是由 Instance 定义的,Singleton 类可以根据名字在一个众所周知的注册表中注册它们的单件实例。这个注册表在字符串名字和单件之间建立映射。当 Instance 需要一个单件时,它参考注册表,根据名字请求单件。注册表查询相应的单件(如果存在的话)并返回它。这个方法使得 Instance 不再需要知道所有可能的 Singleton 类或实例。它所需要的只是所有 Singleton 类的一个公共的接口,该接口包括了对注册表的操作。

讨论

Factory Method 使一个设计可以定制且只略微有一些复杂。其他设计模式需要新的类,而 Factory Method 只需要一个新的操作。人们通常将 Factory Method 作为一种标准的创建对象的方法。但是当被实例化的类根本不发生变化或实例化出现在子类可以很容易重定义的操作(比如初始化操作)中时,这就不必要了。

使用 Abstract Factory、Prototype 或 Builder 的设计甚至比使用 Factory Method 的设计更灵活,但它们也更加复杂。通常,设计以使用 Factory Method 开始,并且当设计者发现需要更大的灵活性时,设计便会向其他创建型模式演化。当你在设计标准之间进行权衡的时候,了解多个模式可以给你提供更多的选择余地。

备案号:闽ICP备2024028309号-1