第二十三章创建型模式.ppt
第二十三章 创建型模式,创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。随着系统演化得越来越依赖于对象组合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(h a r d-c o d i n g)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样,创建有特定行为的对象要求的不仅仅是实例化一个类。在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。整个系统关于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在什么被创建,谁创建它,它是怎样被创建的,以及何时创建这些方面给予你很大的灵活性。它们允许你用结构和功能差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。,因为创建型模式紧密相关,我们将所有5个模式一起研究以突出它们的相似点和相异点。我们也将举一个通用的例子为一个电脑游戏创建一个迷宫来说明它们的实现。这个迷宫和游戏将随着各种模式不同而略有区别。有时这个游戏将仅仅是找到一个迷宫的出口;在这种情况下,游戏者可能仅能见到该迷宫的局部。有时迷宫包括一些要解决的问题和要战胜的危险,并且这些游戏可能会提供已经被探索过的那部分迷宫地图。我们将忽略许多迷宫中的细节以及一个迷宫游戏中有一个还是多个游戏者。我们仅关注迷宫是怎样被创建的。我们将一个迷宫定义为一系列房间,一个,房间知道它的邻居;可能的邻居要么是另一个房间,要么是一堵墙、或者是到另一个房间的一扇门。类R o o m、D o o r和Wa l l定义了我们所有的例子中使用到的构件。我们仅定义这些类中对创建一个迷宫起重要作用的一些部分。我们将忽略游戏者、显示操作和在迷宫中四处移动操作,以及其他一些重要的却与创建迷宫无关的功能。下图表示了这些类之间的关系。,每一个房间有四面,我们使用C+中的枚举类型D i r e c t i o n来指定房间的东南西北:enum Direction North,South,East,West;,类M a p S i t e是所有迷宫组件的公共抽象类。为简化例子,M a p S i t e仅定义了一个操作E n t e r,它的含义决定于你在进入什么。如果你进入一个房间,那么你的位置会发生改变。如果你试图进入一扇门,那么这两件事中就有一件会发生:如果门是开着的,你进入另一个房间。如果门是关着的,那么你就会碰壁。,E n t e r为更加复杂的游戏操作提供了一个简单基础。例如,如果你在一个房间中说“向东走”,游戏,只能确定直接在东边的是哪一个M a p S i t e并对它调用E n t e r。特定子类的E n t e r操作将计算出你的位置是发生改变,还是你会碰壁。在一个真正的游戏中,E n t e r可以将移动的游戏者对象作为一个参数。R o o m是M a p S i t e的一个具体的子类,而 M a p S i t e定义了迷宫中构件之间的主要关系。R o o m有指向其他M a p S i t e对象的引用,并保存一个房间号,这个数字用来标识迷宫中的房间。,下面的类描述了一个房间的每一面所出现的墙壁或门。,我们不仅需要知道迷宫的各部分,还要定义一个用来表示房间集合的M a z e类。用R o o m N o操作和给定的房间号,M a z e就可以找到一个特定的房间。,R o o m N o可以使用线形搜索、h a s h表、甚至,一个简单数组进行一次查找。但我们在此处并不考虑这些细节,而是将注意力集中于如何指定一个迷宫对象的构件上。我们定义的另一个类是M a z e G a m e,由它来创建迷宫。一个简单直接的创建迷宫的方法是使用一系列操作将构件增加到迷宫中,然后连接它们。例如,下面的成员函数将创建一个迷宫,这个迷宫由两个房间和它们之间的一扇门组成:,考虑到这个函数所做的仅是创建一个有两个房间的迷宫,它是相当复杂的。显然有办法使它变得更简单。例如,R o o m的构造器可以提前用墙壁来初始化房间的每一面。但这仅仅是将代码移到了其他地方。这个成员函数真正的问题不在于它的大小而在于它不灵活。它对迷宫的布局进行硬编码。改变布局意味着改变这个成员函数,或是重定义它这意味着重新实现整个过程或是对它的部分进行改变这容易产生错误并且不利于重用。创建型模式显示如何使得这个设计更灵活,但未必会更小。特别是,它们将便于修改定义一个迷宫构件的类。,假设你想在一个包含(所有的东西)施了魔法的迷宫的新游戏中重用一个已有的迷宫布局。施了魔法的迷宫游戏有新的构件,像D o o r N e e d i n g S p e l l,它是一扇仅随着一个咒语才能被锁上和打开的门;以及E n c h a n t e d R o o m,一个可以有不寻常东西的房间,比如魔法钥匙或是咒语。你怎样才能较容易的改变C r e a t e M a z e以让它用这些新类型的对象创建迷宫呢?这种情况下,改变的最大障碍是对被实例化的类进行硬编码。创建型模式提供了多种不同方法从实例化它们的代码中除去对这些具体类的显式引用:,如果C r e a t e M a z e调用虚函数而不是构造器来创建它需要的房间、墙壁和门,那么你可以创建一个 M a z e G a m e的子类并重定义这些虚函数,从而改变被实例化的类。这一方法是Factory Method模式的一个例子。如果传递一个对象给C r e a t e M a z e作参数来创建房间、墙壁和门,那么你可以传递不同的参数来改变房间、墙壁和门的类。这是Abstract Factory模式的一个例子。如果传递一个对象给C r e a t e M a z e,这个对象可以在它所建造的迷宫中使用增加房间、墙壁和门的,操作,来全面创建一个新的迷宫,那么你可以使用继承来改变迷宫的一些部分或该迷宫被建造的方式。这是B u i l d e r模式的一个例子。如果C r e a t e M a z e由多种原型的房间、墙壁和门对象参数化,它拷贝并将这些对象增加到迷宫中,那么你可以用不同的对象替换这些原型对象以改变迷宫的构成。这是P r o t o t y p e模式的一个例子。剩下的创建型模式,S i n g l e t o n,可以保证每个游戏中仅有一个迷宫而且所有的游戏对象都可以迅速访问它不需要求助于全局变量或函数。S i n g l e t o n也使得迷宫易于扩展或替换,且不需变动已有的代码。,3.1 ABSTRACT FACTORY(抽象工厂)对象创建型模式 1.意图 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。2.别名 K i t 3.动机 考虑一个支持多种视感标准的用户界面工具包,例如M o t i f和Presentation Manager。不同的视感风格为诸如滚动条、窗口和按钮等用户界面“窗口组件”,定义不同的外观和行为。为保证视感风格标准间的可移植性,一个应用不应该为一个特定的视感外观硬编码它的窗口组件。在整个应用中实例化特定视感风格的窗口组件类将使得以后很难改变视感风格。为解决这一问题我们可以定义一个抽象的 Wi d g e t F a c t o r y类,这个类声明了一个用来创建每一类基本窗口组件的接口。每一类窗口组件都有一个抽象类,而具体子类则实现了窗口组件的特定视感风格。对于每一个抽象窗口组件类,WidgetFactory接口都有一个返回新窗口组件对象的操作。客户调用这些操作以获得窗口组件实例,但客户并不知道他们正在使用的是哪些具体类。这样客户就不依赖于特定,的视感风格,如下图所示。,每一种视感标准都对应于一个具体的WidgetFactory子类。每一子类实现那些用于创建合适视感风格的窗口组件的操作。例如,MotifWidgetFactory的C r e a t e S c r o l l B a r操作实例化并返回一个M o t i f滚动条,而相应的PMWidgetFactory操作返回一个Presentation Manager的滚动条。客户仅通过WidgetFactory接口创建窗口组件,他们并不知道哪些类实现了特定视感风格的窗口组件。换言之,客户仅与抽象类定义的接口交互,而不使用特定的具体类的接口。WidgetFactory也增强了具体窗口组件类之间依赖关系。一个M o t i f的滚动条应该与M o t i f按钮、,M o t i f正文编辑器一起使用,这一约束条件作为使用MotifWidgetFactory的结果被自动加上。4.适用性 在以下情况可以使用Abstract Factory模式:一个系统要独立于它的产品的创建、组合和表示时。一个系统要由多个产品系列中的一个来配置时。当你要强调一系列相关的产品对象的设计以便进行联合使用时。当你提供一个产品类库,而只想显示它们的,接口而不是实现时。5.结构 此模式的结构如下图所示。,6.参与者 A b s t r a c t F a c t o r y(Wi d g e t F a c t o r y)声明一个创建抽象产品对象的操作接口。C o n c r e t e F a c t o r y(MotifWidgetFactory,P M Wi d g e t F a c t o r y)实现创建具体产品对象的操作。A b s t r a c t P r o d u c t(Wi n d o w s,ScrollBar)为一类产品对象声明一个接口。C o n c r e t e P r o d u c t(M o t i f Wi n d o w,M o t i f S c r o l l B a r),定义一个将被相应的具体工厂创建的产品对象。实现A b s t r a c t P r o d u c t接口。C l i e n t 仅使用由A b s t r a c t F a c t o r y和AbstractProduct类声明的接口。7.协作 通常在运行时刻创建一个ConcreteFactroy类的实例。这一具体的工厂创建具有特定实现的产品对象。为创建不同的产品对象,客户应使用不同的具体工厂。,AbstractFactory将产品对象的创建延迟到它的 C o n c r e t e F a c t o r y子类。8.效果 AbstractFactory模式有下面的一些优点和缺点:1)它分离了具体的类 Abstract Factory模式帮助你控制一个应用创建的对象的类。因为一个工厂封装创建产品对象的责任和过程,它将客户与类的实现分离。客户通过它们的抽象接口操纵实例。产品的类名也在具体工厂的实现中被分离;它们不出现在客户代码中。,2)它使得易于交换产品系列 一个具体工厂类在一个应用中仅出现一次即在它初始化的时候。这使得改变一个应用的具体工厂变得很容易。它只需改变具体的工厂即可使用不同的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻改变。在我们的用户界面的例子中,我们仅需转换到相应的工厂对象并重新创建接口,就可实现从M o t i f窗口组件转换为Presentation Manager窗口组件。3)它有利于产品的一致性 当一个系列中的产品对象被设计成一起工作时,一个应用一次只能使用同一个系列中的对象,这一点很重要。而,AbstractFactory很容易实现这一点。4)难以支持新种类的产品 难以扩展抽象工厂以生产新种类的产品。这是因为AbstractFactory接口确定了可以被创建的产品集合。支持新种类的产品就需要扩展该工厂接口,这将涉及AbstractFactory类及其所有子类的改变。我们会在实现一节讨论这个问题的一个解决办法。9.实现 下面是实现AbstractFactory模式的一些有用技术:1)将工厂作为单件 一个应用中一般每个产品系列只需一个C o n c r e t e F a c t o r y的实例。因此,工厂通常最好实现为一个S i n g l e t o n。2)创建产品 AbstractFactory仅声明一个创建产品的接口,真正创建产品是由ConcreteFactory子类实现的。最通常的一个办法是为每一个产品定义一个工厂方法(参见Factory Method)。一个具体的工厂将为每个产品重定义该工厂方法以指定产品。虽然这样的实现很简单,但它却要求每个产品系列都要有一个新的具体工厂子类,即使这些产品系列的差别很小。如果有多个可能的产品系列,具体工厂也可以使用P r o t o t y p e模式来实现。具体工厂使用产品系列中每一个产品的原型实例来初始化,且它通过复制,它的原型来创建新的产品。在基于原型的方法中,使得不是每个新的产品系列都需要一个新的具体工厂类。3)定义可扩展的工厂 AbstractFactory通常为每一种它可以生产的产品定义一个操作。产品的种类被编码在操作型构中。增加一种新的产品要求改变AbstractFactory的接口以及所有与它相关的类。一个更灵活但不太安全的设计是给创建对象的操作增加一个参数。该参数指定了将被创建的对象的种类。它可以是一个类标识符、一个整数、一个字符串,或其他任何可以标识这种产品的东西。实际上使用这种方法,AbstractFactory只需要一个“Make”操作和一个指示要创建对象的种类的参数。这是前面已经讨论过的基于,原型的和基于类的抽象工厂的技术。与C+这样的静态类型语言相比,这一变化更容易用在类似于S m a l l t a l k这样的动态类型语言中。仅当所有对象都有相同的抽象基类,或者当产品对象可以被请求它们的客户安全的强制转换成正确类型时,你才能够在C+中使用它。Factory Method的实现部分说明了怎样在C+中实现这样的参数化操作。该方法即使不需要类型强制转换,但仍有一个本质的问题:所有的产品将返回类型所给定的相同的抽象接口给客户。客户将不能区分或对一个产品的类别进行安全的假定。可能一个客户需要进行与特定子类,相关的操作,而这些操作却不能通过抽象接口得到。虽然客户可以实施一个向下类型转换(downcast)(例如在C+中用dynamic_cast),但这并不总是可行或安全的,因为向下类型转换可能会失败。这是一个典型的高度灵活和可扩展接口的权衡折衷。10.代码示例 我们将使用Abstract Factory模式创建我们在这章开始所讨论的迷宫。类M a z e F a c t o r y可以创建迷宫的组件。它建造房间、墙壁和房间之间的门。它可以用于一个从文件中读取迷宫说明图并建造相应迷宫的程序。或者它,可以被用于一个随机建造迷宫的程序。建造迷宫的程序将M a z e F a c t o r y作为一个参数,这样程序员就能指定要创建的房间、墙壁和门等类。,回想一下建立一个由两个房间和它们之间的门组成的小迷宫的成员函数CreateMaze。CreateMaze对类名进行硬编码,这使得很难用不同的组件创建迷宫。这里是一个以MazeFactory为参数的新版本的CreateMaze,它修改了以上缺点:,我们创建MazeFactory的子类EnchantedMazeFactory,这是一个创建施了魔法的迷宫的工厂。EnchantedMazeFactory将重定义不同的成员函数并返回Room,Wall等不同的子类。,现在假设我们想生成一个迷宫游戏,在这个游戏里,每个房间中可以有一个炸弹。如果这个炸弹爆炸,它将(至少)毁坏墙壁。我们可以生成一个R o o m的子类以明了是否有一个炸弹在房间中以及该炸弹是否爆炸了。我们也将需要一个Wa l l的子类以明了对墙壁的损坏。我们将称这些类为RoomWithABomb和BombedWall。我们将定义的最后一个类是BombedMazeFactory,它是MazeFactory的子类,保证了墙壁是BombedWall类的而房间是RoomWithABomb的。BombedMazeFactory仅需重定义两个函数:,为建造一个包含炸弹的简单迷宫,我们仅用 B o m b e d M a z e F a c t o r y调用C r e a t e M a z e。,CreateMaze也可以接收一个EnchantedMazeFactory实例来建造施了魔法的迷宫。注意MazeFactory仅是工厂方法的一个集合。这是最通常的实现Abstract Factory模式的方式。同时注意MazeFactory不是一个抽象类;因此它既作为Abstract Factory也作为ConcreteFactory。这是Abstract Factory 模式的简单应用的另一个通常的实现。因为MazeFactory是一个完全由工厂方法组成的具体类,通过生成一个子类并重定义需要改变的操作,它很容易生成一个新的MazeFactory。CreateMaze使用房间的SetSide操作以指定它们的,各面。如果它用一个BombedMazeFactory创建房间,那么该迷宫将由有BombedWall 面的RoomWithABomb 对象组成。如果RoomWithABomb必须访问一个BombedWall的与特定子类相关的成员,那么它将不得不对它的墙壁保存一个引用,并进行从Wall*到BombedWall*的转换。只要该参数确实是一个BombedWall,这个向下类型转换就是安全的,而如果墙壁仅由一个BombedMazeFactory创建就可以保证这一点。11.已知应用 I n t e r Vi e w使用“K i t”后缀来表示A b s t r a c t F a c t o r y类。它定义Wi d g e t K i t和D i a l o g K i t,抽象工厂来生成与特定视感风格相关的用户界面对象。I n t e r Vi e w还包括一个L a y o u t K i t,它根据所需要的布局生成不同的组成(c o m p o s i t i o n)对象。例如,一个概念上是水平的布局根据文档的定位(画像或是风景)可能需要不同的组成对象。E T+使用Abstract Factory模式以达到在不同窗口系统(例如,X Wi n d o w s和S u n Vi e w)间的可移植性。Wi n d o w S y s t e m抽象基类定义一些接口,来创建表示窗口系统资源的对象(例如 M a k e Wi n d o w、M a k e F o n t、M a k e C o l o r)。具体的子类为某个特定的窗口系统实现这些接口。运行时刻,E T+创建一个具体,WindowSystem子类的实例,以创建具体的系统资源对象。12.相关模式 Abstract Factory类通常用工厂方法(Factory Method)实现,但它们也可以用P r o t o t y p e实现。一个具体的工厂通常是一个单件(S i n g l e t o n)。,作业:1.创建型模式中不断出现的两个主旋律是什么?2.迷宫游戏中,构造函数创建迷宫的代码如下:,简述这一段代码有什么问题?引入设计模式后会让它变得更简单吗?为什么引入设计模式?3.Abstract Factory模式的适用情况有哪些?4.假设已经定义了迷宫游戏类MazeGame和包含炸弹的迷宫工厂类BombedMazeFactory,写出由迷宫游戏类创建含有炸弹的迷宫的代码。,