创建型模式-设计模式
创建型设计模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
随着系统演化得越来越依赖于对象组合而不是继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(hard- coding)转移为定义一个较小的基本行为集,这些行为可以被组合成仁义数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类。
在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起。整个系统关于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在
什么
被创建、
谁
创建它、它是
怎么
被创建的,以及
何时
创建等方面给予很大的灵活性。它们允许你用结构和功能差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时指定)。
有时创建型模式是互相竞争的。例如,有些情况下
Prototype
或
Abstract Factory
用起来都很好。而在有些情况下它们是互补的:
Builder
可以使用其他模式去实现某个构件的创建;Prototype 可以在它的实现中使用 Singleton。
下面针对创建迷宫这个例子讲述每个模式:
我们仅关注迷宫怎样创建的。我们将一个迷宫定义为一系列房间,一个房间知道它的邻居;可能的邻居要么是另一个房间,要么是一堵墙或者是到另一个房间的一扇门。每一个房间有四面。
类Room、Door和Wall定义了我们所有的例子中用到的构件。类MapSite是所有迷宫构件的公共抽象类。
/// <summary> ///迷宫构件基类/// </summary> public classMapSite
{publicMapSite()
{
}public virtual voidEnter()
{
}
}
/// <summary> ///房间类/// </summary> public classRoom: MapSite
{private int_roomNo;private Dictionary<DirectionEnum,MapSite>_sides;public Room(introomNo)
{
_roomNo=roomNo;
}public int RoomNo() =>_roomNo;public voidSetSide(DirectionEnum direction,MapSite mapSite)
{
_sides.Add(direction, mapSite);
}publicMapSite GetSide(DirectionEnum direction)
{return_sides[direction];
}
}/// <summary> ///房间四面方向枚举/// </summary> public enumDirectionEnum
{
东,南,西,北
}
/// <summary> ///墙/// </summary> public classWall:MapSite
{publicWall()
{
}
}
/// <summary> ///门/// </summary> public classDoor:MapSite
{privateRoom _room1;privateRoom _room2;publicDoor(Room room1,Room room2)
{
_room1=room1;
_room2=room2;
}
}
/// <summary> ///迷宫类/// </summary> public classMaze
{publicMaze()
{
}private HashSet<Room> rooms = new HashSet<Room>();public voidAddRoom(Room room)
{
rooms.Add(room);
}public Room GetRoomByNo(intnum)
{return rooms.FirstOrDefault(r=>r.RoomNo() ==num);
}
}
下面我们定义另一个类 Maze Game,由它来创建迷宫。一个简单直接的创建迷宫的方法是使用一系列操作将构件增加到迷宫中,然后连接它们。例如,下面的成员函数将创建一个迷宫,这个迷宫由两个房间和它们之间的一扇门组成:
public classMazeGame
{publicMazeGame()
{
}publicMaze CreateMaze()
{
Maze maze= newMaze();
Room r1= new Room(1);
Room r2= new Room(2);
Door door= newDoor(r1,r2);
maze.AddRoom(r1);
maze.AddRoom(r2);
r1.SetSide(DirectionEnum.东,newWall());
r1.SetSide(DirectionEnum.北, door);
r1.SetSide(DirectionEnum.南,newWall());
r1.SetSide(DirectionEnum.西,newWall());
r2.SetSide(DirectionEnum.东,newWall());
r2.SetSide(DirectionEnum.北,newWall());
r2.SetSide(DirectionEnum.南, door);
r2.SetSide(DirectionEnum.西,newWall());returnmaze;
}
}
这个成员函数真正的问题不在于它的大小而在于它
不灵活
。很像面向过程编程,而不是面向对象。它对迷宫的布局进行硬编码。改变布局意味着改变这个函数,通过以下方式:重定义它——意味着重新实现整个过程;对它的部分进行改变——这容易产生错误并且不利于复用。
创建型模式表明如何使得整个设计更灵活,但未必会更小。特别是,它们将便于修改定义迷宫构件的类。
假设你想在一个包含(所有东西)施了魔法的迷宫的新游戏中复用一个已有的迷宫布局。施了魔法的迷宫游戏有新的构件,如DoorNeedingSpell,它是一扇只能用咒语才能被锁上和打开的门;以及EnchantedRoom,一个可以有不寻常东西的房间,比如魔法钥匙或者咒语。
你怎样才能比较容易地改变CreateMaze 以让它用这些新类型的对象创建迷宫呢?
这种情况下,改变的最大障碍是对已实例化的类进行硬编码。创建型模式提供了多种不同方法,
从实例化它们的代码中除去这些具体类的显式引用
:
- 如果 CreateMaze 调用虚函数而不是构造器来创建它需要的房间、墙壁和门,那么可以创建一个MazeGame的子类并重定义这些虚函数,从而改变被实例化的类。这一方法是Factory Method 模式的一个例子。
- 如果传递一个对象给 CreateMaze 作为参数来创建房间、墙壁和门,那么可以传递不同的参数来改变房间、墙壁和门的类。这是 Abstract Factory 模式的一个例子。
- 如果传递一个对象给 CreateMaze,这个对象可以在它所建造的迷宫中使用增加房间、墙壁和门的操作来全面创建一个新的迷宫,那么可以使用继承来改变迷宫的一些部分或迷宫的建造方式。这是 Builder 模式的一个例子。
- 如果 CreateMaze 由多种原型的房间、墙壁和门对象参数化,它复制并将这些对象增加到迷宫中,那么可以用不同的对象替换这些原型对象以改变迷宫的构成。这是 Prototype 模式的一个例子。
剩下的创建型模式 Singleton 可以保证每个游戏中仅有一个迷宫而且所有的对象都可以快速访问它——不需要求助于全局变量或函数。Singleton 也使得迷宫易于扩展或替换,且不需要变动已有的代码。
一、Abstract Factory(抽象工厂)—— 对象创建模型
1、意图
提供一个接口以创建一系列相关或相互依赖的对象,而无须指定它们具体的类。
2、动机
考虑一个支持多种视感标准的用户界面工具包,例如 Motif 和 Presentation Manager。不同的视感风格为诸如滚动条、窗口和按钮等用户界面“窗口组件”定义不同的外观和行为。一个应用不应该为一个特定的视感外观硬编码它的窗口组件。在整个应用中实例化特定视感风格的窗口组件将使得以后很难改变视感风格。
为解决这一问题,我们可以定义一个抽象的 WidgetFactory 类,这个类声明了一个用来创建每一类基本窗口组件的接口。每一类窗口组件都有一个抽象类,而具体子类则实现了窗口组件的特定视感风格。对于每一个抽象窗口组件类,WidgetFactory 接口都有一个返回新窗口组件对象的操作。客户调用这些操作以获得窗口组件实例,但客户并不知道其正在使用的是哪些具体类。这样客户就不依赖于一般的视感风格,如下图所示。
每一种视感标准都对应于一个具体的 WidgetFactory 子类。每一子类实现那些用于创建合适视感风格的窗口组件的操作。例如,MotifWidgetFactory 的 CreateScrollBar 操作实例化并返回一个 Motif 滚动条,而相应的 PMWidgetFactory 操作返回一个Presentation Manager 的滚动条。客户仅通过 WidgetFactory 接口创建窗口组件,而并不知道哪些类实现了特定视感风格的窗口组件。换言之,客户仅与抽象类定义的接口交互,而不使用特定的具体类的接口。
WidgetFactory 也增强了具体窗口组件类之间的依赖关系。一个 Motif 的滚动条应该与 Motif按钮、Motif文本编辑器一起使用,这一约束条件作为使用 MotifWidgetFactory 的结果被自动加上。
3、适用性
在以下情况使用 Abstract Factory 模式:
- 一个系统要独立于它的产品的创建、组合和表示。
- 一个系统要由多个产品系列中的一个来配置。
- 要强调一系列相关的产品对象以便进行联合使用。
- 提供一个产品类库,但只想显示它们的接口而不是实现。
4、结构
5、参与者
- AbstractFactory
--声明一个创建抽象产品对象的操作接口。
- ConcreteFactory (MotifWidgetFactory, PMWidgetFactory)
--实现创建具体产品对象的操作。
- AbstractProduct (Windows, ScrollBar)
一为一类产品对象声明一个接口。
- Concrete Product (MotifWindow, MotifScrollBar)
一定义一个将被相应的具体工厂创建的产品对象。
--实现AbstractProduct接口。
- Client
--仅使用由AbstractFactory和AbstractProduct类声明的接口。
6、协作
- 通常在运行时创建一个 ConcreteFactory 类的实例。这一具体的工厂创建具有特定实现的产品对象。为创建不同的产品对象,客户应使用不同的具体工厂。
- AbstractFactory 将产品对象的创建延迟到它的 ConcreteFactory 子类。
7、效果
AbstractFactory 模式有以下优点和缺点:
(1)
它分离了具体的类
Abstract Factory 模式帮助你控制一个应用创建的对象的类。因为一个工厂封装创建产品对象的责任和过程,它将客户与类的实现分离。客户通过它们的抽象接口操纵实例。产品的类名也在具体工厂的实现中被隔离,即它们不出现在客户代码中。
(2)
它使得易于交换产品系列
一个具体工厂类在一个应用中仅出现一次——在它初始化的时候
。这使得改变一个应用的具体工厂变得很容易。只需要改变具体的工厂即可使用不同的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻改变。
(3)
它有利于产品的一致性
当一个系列中的产品对象被设计成一起工作时,一个应用一次只能使用同一系列中的对象,这一点很重要。而 AbstractFactory 很容易实现这一点。
(4)
难以支持新种类的产品
难以扩展抽象工厂以生产新种类的产品。这是因为 AbstractFactory 接口确定了可以被创建的产品集合。支持新种类的产品就需要扩展该工厂接口,这将涉及 AbstractFactory 类及其所有子类的概念。在下面实现部分讨论解决这个问题的办法。
8、实现
下面是实现 Abstract Factory 模式的一些有用的技术:
(1)
将工厂作为单件
一个应用中一般每个产品系列之需要一个 ConcreteFactory 的实例。因此工厂通常最好实现为一个 Singleton。
(2)
创建产品
AbstractFactory 仅声明一个创建产品的接口,真正创建产品是由 ConcreteProduct 子类实现的。最通常的办法是为每一个产品定义一个工厂方法(见 Factory Method)。一个具体的工厂将为每个产品重定义该工厂方法以指定产品。虽然这样很简单,但它却要求每个产品系列都要有一个新的具体工厂子类,即使这些产品系列的差别很小。
如果有多个可能的产品系列,具体工厂也可以使用 prototype模式来实现。具体工厂使用产品系列中每一个产品的原型实例来初始化,且它通过复制它的原型来创建新的产品。基于原型的方法使得并非每个新的产品系列都需要一个新的具体工厂类。
(3)
定义可扩展的工厂
AbstractFactory 通常为每一种它可以生产的产品定义一个操作。产品的种类被编码在操作型构中。增加一种新的产品要求改变 AbstractFactory 的接口以及所有与它相关的类。
一个更灵活但不太安全的设计是给创建对象的操作增加一个参数。该参数指定了将被创建的对象的种类。它可以是一个类标识符、一个整数、一个字符串,或其他任何可以表示这种产品的东西。实际上,使用这种方法 AbstractFactory 只需要一个 Make 操作和一个指示要创建对象的种类的参数。这是前面已经讨论过的基于原型的和基于类的抽象工厂的技术。
9、代码示例
/// <summary> ///抽象工厂基类/// </summary> public classMazeFactory
{publicMazeFactory()
{
}public virtualMaze MakeMaze()
{return newMaze();
}public virtualWall MakeWall()
{return newWall();
}public virtual Room MakeRoom(intnum)
{return newRoom(num);
}public virtualDoor MakeDoor(Room r1, Room r2)
{return newDoor(r1,r2);
}
}
最前面创建迷宫的方法 CreateMaze 中,CreateMaze 对类名进行硬编码,这使得很难用不同的构件创建迷宫。
这里是一个以 MazeFactory 为参数的新版本的 CreateMaze,它修改了以上缺点:
publicMaze CreateMaze()
{
Maze maze= newMaze();
Room r1= new Room(1);
Room r2= new Room(2);
Door door= newDoor(r1,r2);
maze.AddRoom(r1);
maze.AddRoom(r2);
r1.SetSide(DirectionEnum.东,newWall());
r1.SetSide(DirectionEnum.北, door);
r1.SetSide(DirectionEnum.南,newWall());
r1.SetSide(DirectionEnum.西,newWall());
r2.SetSide(DirectionEnum.东,newWall());
r2.SetSide(DirectionEnum.北,newWall());
r2.SetSide(DirectionEnum.南, door);
r2.SetSide(DirectionEnum.西,newWall());returnmaze;
}/// <summary> ///使用抽象工厂的方法/// </summary> /// <param name="mazeFactory"></param> /// <returns></returns> publicMaze CreateMaze(MazeFactory mazeFactory)
{
Maze maze=mazeFactory.MakeMaze();
Room r1= mazeFactory.MakeRoom(1);
Room r2= mazeFactory.MakeRoom(2);
Door door=mazeFactory.MakeDoor(r1, r2);
maze.AddRoom(r1);
maze.AddRoom(r2);
r1.SetSide(DirectionEnum.东, mazeFactory.MakeWall());
r1.SetSide(DirectionEnum.北, door);
r1.SetSide(DirectionEnum.南, mazeFactory.MakeWall());
r1.SetSide(DirectionEnum.西, mazeFactory.MakeWall());
r2.SetSide(DirectionEnum.东, mazeFactory.MakeWall());
r2.SetSide(DirectionEnum.北, mazeFactory.MakeWall());
r2.SetSide(DirectionEnum.南, door);
r2.SetSide(DirectionEnum.西, mazeFactory.MakeWall());returnmaze;
}
我们创建 MazeFactory 的子类 EnchantedMazeFactory ,这是一个创建施了魔法的迷宫的工厂。EnchantedMazeFactory 将重定义不同的成员函数并返回 Room、Wall 等不同的子类。
//<summary> ///施了魔法的迷宫的工厂/// </summary> public classEnchantedMazeFactory:MazeFactory
{publicEnchantedMazeFactory()
{
}public override Room MakeRoom(intnum)
{return newEnchantedRoom(num);
}public overrideDoor MakeDoor(Room r1, Room r2)
{return newDoorNeedingSpell(r1, r2);
}
}
CreateMaze 可以用一个 EnchantedMazeFactory 实例来建造施了魔法的迷宫。
注意 MazeFactory 仅是工厂方法的一个集合。这是通常的实现 Abstract Factory 模式的方式。同时注意 MazeFactory 不是一个抽象类,因此它既作为 AbstractFactory 也作为 ConcreteFactory 。这是 Abstract Factory 模式的简单应用的另一个通常的实现。
AbstractFactory 类通常用工厂方法(Factory Method)实现,但她们也可以用 Prototype 实现。
一个具体的工厂通常是一个单件(Singleton)。
二、Builder(生成器)——对象创建模式
1、意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
2、动机
一个RTF文档交换格式的阅读器应能将RTF转换为多种文本格式。该阅读器可以将RTF文档转换成普通ASCII文本或转换成一个能以交互方式编辑的文本窗口组件。但问题在于可能转换的数目是无限的。因此要能够很容易实现新的转换的增加,同时又不改变RTF阅读器。
一个解决办法是用一个可以将RTF转换成另一种文本表示的 TestConverter 对象来配置这个 RTFReader 类。当 RTFReader 对 RTF 文档进行语法分析时,它使用 TestConverter 去做转换。无论何时 RTFReader 识别了一个RTF标记(或是普通文本或是一个RTF控制字),它都发送一个请求给 TestConverter 去转换这个标记。TestConverter 对象负责进行数据转换以及用特定格式表示该标记,如图。
TestConverter 的子类对不同转换和不同格式进行特殊处理。例如,一个 ASCIIConverter 只负责转换普通文本,而忽略其他转换请求。另一方面,一个 TeXConverter 将会实现对所有请求的操作,以便生成一个获取文本中组呕呕风格信息的TEX表示。一个TextWidgetConverter将生成一个复杂的用户界面对象以便用户浏览和编辑文本。
每种转换器类将创建和装配一个复杂对象的机制隐含在抽象接口的后面。转换器独立于阅读器,阅读器负责对一个RTF文档进行语法分析。
Builder 模式描述了所有这些关系。每一个转换器类在该模式中被称为
生成器
(builder),而阅读器则称为
导向器
(director)。在上面的例子中,Builder模式将分析文本格式的算法(即RTF文档的语法分析程序)与描述怎样创建和表示一个转换后格式的算法分离开来。这使我们可以复用 RTFReader 的语法分析算法,根据 RTF 文档创建不同的文本表示——仅需使用不同的 TestConverter 的子类配置该 RTFReader 即可。
3、适用性
在以下情况使用 Builder 模式:
- 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
- 当构造过程必须允许被构造的对象有不同的表示时。
4、结构
此模式的结构如下图所示。
5、参与者
- Builder(TextConverter)
— 为创建一个 Product 对象的各个部件指定抽象接口。
- ConcreteBuilder(ASCIIConverter、...)
— 实现 Builder 的接口以及构造和装配该产品的各个部件。
— 定义并跟踪它所创建的表示。
— 提供一个检索产品的接口(例如,GetASCIIText和GetTextWidget)。
- Director(RTFReader)
— 构造一个使用 Builder 接口的对象。
- Product(ASCIIText、...)
— 表示被构造的对象。ConcreteBuilder 创建该产品的内部表示并定义它的装配过程。
— 包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
6、协作
- 客户创建 Director 对象,并用它所想要的 Builder 对象进行配置。
- 一旦生成了产品部件,导向器就会通知生成器。
- 生成器处理导向器的请求,并将部件添加到该产品中。
- 客户从生成器中检索产品。
下面的交互图说明了 Builder 和 Director 是如何与一个客户协作的。
7、效果
这里是 Builder 模式的主要效果:
(1)
它使你可以改变一个产品的内部表示
Builder 对象提供给导向器一个构造产品的抽象接口。该接口使得生成器可以隐藏这个产品的表示和内部结构。它同时隐藏了该产品是如何装配的。因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定义一个新的生成器。
(2)
它将构造代码和表示代码分开
Builder 模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。
客户不需要知道定义产品结构的类的所有信息,这些类是不出现在 Builder 接口中的
。每个 ConcreteBuilder 包含了创建和装配一个特定产品的所有代码。这些代码只需要写一次;然后不同的 Director 可以复用它以在相同部件集合的基础上构建不同的 Product。在前面的 RTF 例子中,我们可以为RTF格式以外的格式定义个阅读器,比如一个SGMLReader,并使用相同的 TextConverter 生成SGML文档的 ASCIIText、TexText和TextWidget译本。
(3)
它使你可对构造过程进行更精细的控制
Builder 模式与一下子就生成产品的创建模式不同,它是在向导器的控制下一步一步的构造产品的。仅当该产品完成时向导器材=才从生成器中取回它。因此 Builder 接口相比其他创建型模式能更好地反映产品的构造过程。这使你可以更精细地控制构建过程,从而能更精细地控制所得产品的内部结构。
8、实现
通常有一个抽象的 Builder 类为导向器可能要求创建的每一个构件定义一个操作。这些操作缺省情况下什么都不做。一个 ConcreteBuilder 类对它有兴趣创建的构件重定义这些操作。
这里是其他一些要考虑的实现问题:
(1)
装配和构造接口
生成器逐步地构造它们的产品。因此 Builder 类接口必须足够普遍,以便为各种类型的具体生成器构造产品。
一个关键的设计问题在于构造和装配过程的模型。构造请求的结果只是被添加到产品中,通常这样的模型就已足够了。在 RTF 的例子中,生成器转换下一个标记并将它添加到它已经转换了的文本中。
但有时你可能需要访问前面已经构造了的产品部件。我们在代码示例一节所给出的 Maze 例子中,MazeBuilder 接口允许你在已经存在的房间之间增加一扇门。在这种情况下,生成器会将构件返回给导向器。
(2)
为什么产品没有抽象类
通常情况下,由具体生成器生成的产品,其表示相差非常大,以至于给不同的产品以公共父类没有太大意思。在 RTF 例子中,ASCIIText 和 TextWidget 对象不太可能有公共接口,它们也不需要这样的接口。因为客户通常用合适的具体生成器来配置导向器,客户所处的位置使它知道 Builder 的哪一个具体子类被使用,并能相应地处理它的产品。
9、代码示例
我们将定义一个 CreateMaze 成员函数的变体,它以类 MazeBuilder 的一个生成器对象作为参数。
MazeBuilder 类定义下面的接口来创建迷宫:
/// <summary> ///迷宫生成器父类/// </summary> public classMazeBuilder
{publicMazeBuilder()
{
}public virtual voidBuildMaze()
{
}public virtual void BuildRoom(introomNo)
{
}public virtual void BuildDoor(int roomFrom, introomTo)
{
}publicMaze GetMaze()
{return null;
}
}
该接口可以创建:(1)迷宫;(2)有一个特定房间号的房间;(3)在有号码的房间之间门。GetMaze 操作返回这个迷宫给客户。MazeBuilder 的子类将重定义这些操作,返回它们做创建的迷宫。
用 MazeBuilder 接口,我们可以改变 CreateMaze 成员函数,以生成器作为它的参数。
publicMaze CreateMaze(MazeBuilder mazeBuilder)
{
mazeBuilder.BuildMaze();
mazeBuilder.BuildRoom(1);
mazeBuilder.BuildRoom(2);
mazeBuilder.BuildDoor(1,2);returnmazeBuilder.GetMaze();
}
将这个 CreateMaze 版本与原来的相比,注意生成器是如何隐藏迷宫的内部表示的——定义房间、门和墙壁的那些类——以及这些部件是如何组装成最终的迷宫的。有人可能猜测到有一些类是用来表示房间和门的,但没有迹象显示哪个类是用来表示墙壁的。这就是使得改变一个迷宫的表示方式要容易一些,
因为所有 MazeBuilder 的客户都不需要被改变
。
子类 StandardMazeBuilder 是一个创建简单迷宫的实现。它将正在创建的迷宫放在变量 _currentMaze 中。
public classStandardMazeBuilder:MazeBuilder
{privateMaze _currentMaze;publicStandardMazeBuilder()
{
}publicMaze GetMaze()
{return_currentMaze;
}/// <summary> ///实例化一个Maze,它将被其他操作装配并返回给客户/// </summary> public override voidBuildMaze()
{
_currentMaze= newMaze();
}/// <summary> ///BuildRoom创建一个房间并建造它周围的墙壁/// </summary> /// <param name="roomNo"></param> public override void BuildRoom(introomNo)
{if (_currentMaze != null)
{
Room room= newRoom(roomNo);
_currentMaze.AddRoom(room);
room.SetSide(DirectionEnum.东,newWall());
room.SetSide(DirectionEnum.北,newWall());
room.SetSide(DirectionEnum.南,newWall());
room.SetSide(DirectionEnum.西,newWall());
}
}/// <summary> ///建造一扇两个房间之间的门,查找迷宫中的这两个房间并找到它们相邻的墙/// </summary> /// <param name="roomFrom"></param> /// <param name="roomTo"></param> public override void BuildDoor(int roomFrom, introomTo)
{
Room r1=_currentMaze.GetRoomByNo(roomFrom);
Room r2=_currentMaze.GetRoomByNo(roomTo);
Door d= newDoor(r1,r2);
r1.SetSide(CommonWall(r1,r2) ,d);
r1.SetSide(CommonWall(r2, r1), d);
}/// <summary> ///一个功能性操作,它决定两个两个房间之间的公共墙壁的方位。/// </summary> /// <param name="r1"></param> /// <param name="r2"></param> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> privateDirectionEnum CommonWall(Room r1, Room r2)
{throw newNotImplementedException();
}
}
客户现在可以用 CreateMaze 和 StandardMazeBuilder 来创建一个迷宫:
Maze maze;
MazeGame mazeGame;
StandardMazeBuilder mazeBuilder;
mazeGame.CreateMaze(mazeBuilder);
maze= mazeBuilder.GetMaze();
我们本可以将所有的 StandardMazeBuilder 操作放在 Maze 中并让每一个 Maze 创建自身,但将Maze变得小一些使得它能更容易被理解和修改,而且 StandardMazeBuilder 易于从 Maze 中分离。
更重要的是,将两者分离使得你可以有多种 MazeBuilder ,每一种使用不同的房间、墙壁和门的类
。
Abstract Factory 与 Builder 相似,因为它也可以复杂对象。主要的区别是 Builder 模式着重于一步步构造一个复杂对象,而 Abstract Factory 着重于多个系列的产品对象(简单的或是复杂的)。Builder 在最后一步返回产品,而对于 Abstract Factory ,产品是立即返回的。
三、Factory Method(工厂方法)—— 对象创建型模式
1、意图
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。
2、别名
虚构造器
3、动机
框架使用抽象类定义和维护对象之间的关系。这些对象的创建通常也由框架负责。
考虑这样一个应用框架,它可以向用户显示多个文档。在这个框架中,两个主要的抽象是类 Application 和 Document。这两个类都是抽象的,客户必须通过它们的子类来做与具体应用相关的实现。例如,为创建一个绘图应用,我们定义类 DrawingApplication 和 DrawingDocument。Application 类负责管理 Document 并根据需要创建它们——例如,当用户从菜单中选择 Open 或 New 的时候。
因为被实例化的特定 Document 子类是与特定应用相关的,所以 Application 类不可能预测到哪个 Document 子类将被实例化——Application类仅知道一个新的文档何时应被创建,而不知道哪种Document将被创建。这就产生了一个尴尬的局面:框架必须实例化类,但是它只知道不能被实例化的抽象类。
Factory Method 模式提供了一个解决方案。它封装了哪个Document子类将被创建的信息并将这些信息从该框架中分离出来,如图:
Application 的子类重定义 Application 的抽象操作 CreateDocument 以返回适当的 Document 子类对象。一旦一个 Application 子类实例化,它就可以实例化与应用相关的文档,而无需知道这些文档的类。我们称 CreateDocument 是一个工厂方法,因为它负责生产一个对象。
4、适用性
在下列情况下可以使用 Factory Method 模式:
- 当一个类不知道它所必须创建的对象的类的时候。
- 当一个类希望由它的子类来指定它所创建的对象的时候。
- 当类将创建对象的职责委托给多个帮助子类的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。
5、结构
6、参与者
Product(Document)
— 定义工厂方法所创建的对象的接口。
ConcreteProduct(MyDocument)
— 实现 Product 接口。
Creator(Application)
— 声明工厂方法,该方法返回一个 Product 类型的对象。Creator 也可以定义一个工厂方法的缺省实现,它返回一个缺省的 ConcreteProduct 对象。
— 可以调用工厂方法以创建一个 Product 对象。
ConcreteCreator(MyApplication)
— 重定义工厂方法以返回一个 ConcreteProduct 对象。
7、效果
工厂方法不再将与特定应用有关的类绑定到你的代码中。代码进处理 Product 接口,因此它可以与用户定义的任何 ConcreteProduct 类一起使用。
工厂方法的一个潜在缺点在于,客户可能仅仅为了创建一个特定的 ConcreteProduct 对象,就不得不创建 Creator 的子类。当 Creator 子类不是必需的时候,客户现在必然处理类演化的其他方面。
下面是 Factory Method 模式的另外两种效果:
(1)
为子类提供钩子(hook)
用工厂方法在一个类的内部创建对象通常比直接创建对象更灵活。Factory Method 给子类一个钩子以提供对象的扩展版本。
在 Document 的例子中,Document 类可以定义一个称为 CreateFileDialog 的工厂方法,该方法为打开一个已有的文档创建默认的文件对话框对象。Document 的子类可以重定义这个工厂方法以定义一个与特定应用相关的文件对话框。在这种情况下,工厂方法就不再抽象了,而是提供了一个合理的缺省实现。
(2)
连接平行的类层次
迄今为止,在我们所考虑的例子中,工厂方法并不只是被 Creator 调用,客户可以找到一些有用的工厂方法,尤其在平行的类层次的情况下。
当一个类将它的一些职责委托给一个独立的类的时候,就产生了平行类层次。
考虑可以被交互操纵的图形,也就是说,可以用鼠标对它们进行伸展、移动或者旋转。实现这样一些交互并不总是那么容易。它通常需要存储和更新在给定时刻记录操纵状态的信息,这个状态仅仅在操纵时需要。因此它不需要被保存在图形对象中。此外,当用户操纵图形时,不同图形有不同的行为。例如,将直线图形拉长可能会产生一个端点被移动的效果,而伸展文本图形可能会改变行距。
有了这些限制,最好使用一个独立的 Manipulator 对象实现交互并保存所需要的任何与特定操纵有关的状态。不同的图形将使用不同的 Manipulator 子类来处理特定的交互。得到的 Manipulator 类层次与 Figure 类层次是平行的(至少部分平行),如图:
Figure 类提供了一个 CreateManipulator 工厂方法,它使得客户可以创建一个与 Figure 相对应的 Manipulator 。Figure 子类重定义该方法以返回一个合适的 Manipulator 子类实例,而 Figure 子类可以只是继承这个缺省实现。这样的 Figure 类不需要相应的 Manipulator 子类——
因此该层次只是部分平行
。
注意工厂方法是怎样定义两个类层次之间的连接的。它将哪些类应一同工作的信息局部化了。
8、实现
当应用 Factory Method 模式时要考虑下面一些问题:
(1)主要有两种不同的情况 Factory Method 模式主要有两种不同的情况 一是,Creator 类是一个抽象类并且不提供它所声明的工厂方法的实现;二是,Creator 是一个具体的类而且为工厂方法提供一个缺省的实现。也有可能有一个定义了缺省实现的抽象类,但这并不常见。
(2)参数化工厂方法 该模式的另一种情况使得工厂可以创建多种产品。工厂方法采用一个标识要被创建的对象种类的参数。工厂方法创建的所有对象将共享 Product 接口。在 Document 的例子中,Application 可能支持不同种类的 Document。你给 CreateDocument 传递一个外部参数来制定将要创建的文档的种类。
一个参数化的工厂具有如下的一般形式,此处 MyProduct 和 YourProduct 是 Product 的子类:
public classCreator
{publicCreator()
{
}public virtualProduct Create(ProductId id)
{if (id ==MINE)
{return newMyProduct();
}if (id ==YOUR)
{return newYourProduct();
}return null;
}
}
重定义一个参数化的工厂使你可以简单而有选择性地扩展或改变一个 Creator 生产的产品。你可以为新产品引入新的标识,或将已有的标识符与不同的产品相关联。
(3)使用模版以避免创建子类 正如我们已经提及的,工厂方法另一个潜在的问题是它们可能仅为了创建适当的 Product 对象而迫使你创建 Creator 子类。可以使用范型,将 Product 类作为参数:
public class StandardCreator<T> where T : Product, new()
{publicT Create()
{return newT();
}
}
(4)命名约定 使用命名约定是一个好习惯,它可以清楚地说明你正在使用工厂方法。
9、代码示例
函数 CreateMaze 建造并返回一个迷宫。这个函数存在的一个问题是它对迷宫、房间、门和墙壁的类进行了硬编码。我们将引入工厂方法使子类可以选择这些构件。首先我们在 MazeGame 中定义工厂方法以创建迷宫、房间、墙壁和门对象,然后用这些工厂方法重写 CreateMaze:
public classMazeGame
{publicMazeGame()
{
}#region 工厂方法 public virtualMaze MakeMaze()
{return newMaze();
}public virtual Room MakeRoom(intn)
{return newRoom(n);
}public virtualWall MakeWall()
{return newWall();
}public virtualDoor MakeDoor(Room r1, Room r2)
{return newDoor(r1,r2);
}#endregion publicMaze CreateMaze()
{
Maze maze=MakeMaze();
Room r1= MakeRoom(1);
Room r2= MakeRoom(2);
Door door=MakeDoor(r1,r2);
maze.AddRoom(r1);
maze.AddRoom(r2);
r1.SetSide(DirectionEnum.北,MakeWall());
r1.SetSide(DirectionEnum.东, door);
r1.SetSide(DirectionEnum.南, MakeWall());
r1.SetSide(DirectionEnum.西, MakeWall());
r2.SetSide(DirectionEnum.北, MakeWall());
r2.SetSide(DirectionEnum.东, MakeWall());
r2.SetSide(DirectionEnum.南, MakeWall());
r2.SetSide(DirectionEnum.西, door);returnmaze;
}
}
不同的游戏可以创建 MazeGame 的子类以特别指明一些迷宫的部件。MazeGame 子类可以重定义一些或所有的工厂方法以指定产品中的变化。例如,一个 BombedMazeGame 可以重定义产品 Room 和 Wall 以返回爆炸后的变体:
public classBombedMazeGame: MazeGame
{publicBombedMazeGame()
{
}public overrideWall MakeWall()
{return newBombedWall();
}public override Room MakeRoom(intn)
{returnRoomWithABomb(n);
}
}
Abstract Factory 经常用工厂方法来实现。Abstract Factory 模式中动机一节的例子也对 Factory Method 进行了说明。
工厂方法通常在 Template Method 中调用。在上面的文档例子中,NewDocument 就是一个模板方法。
Prototype 不需要创建 Creator 的子类。但是,它们需要要求一个针对 Product 类的 Initialize 操作。 Creator 使用Initialize 来初始化对象,而Factory Method 不需要这样的操作。
四、Prototype(原型)
1、意图
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
2、动机
你可以通过定制一个通用的图形编辑器框架以及增加一些表示音符、休止符和五线谱的新对象来构造一个乐谱编辑器。这个编辑器框架可能有一个工具选择板用于将这些音乐对象加到乐谱中。这个选择板可能还包括选择、移动和其他操纵音乐对象的工具。用户可以点击四分音符工具并使用它将四分音符加到乐谱中,或者可以使用移动工具在五线谱上上下移动一个音符,从而改变它的音调。
我们假定该框架为音符和五线谱这样的图形构件提供了一个抽象的 Graphic 类。此外,为定义选择板中的那些工具,还提供了一个抽象类 Tool。该框架还为一些创建图形对象实例并将它们加入文档中的工具预定义了一个 GraphicTool 子类。
但 GraphicTool 给框架设计者带来一个问题。音符和五线谱的类特定于我们的应用,而 GraphicTool 类却属于框架。GraphicTool 不知道如何创建我们的音乐类的实例,并将它们添加到乐谱中。我们可以为每一种音乐对象创建一个 GraphicTool 的子类,但这样会产生大量的子类,这些子类仅仅在它们所初始化的音乐对象的类别上有所不同。我们知道对象组合是比创建子类更灵活的一种选择。
问题是,该框架怎样用它来参数化 GraphicTool 的实例,而这些实例是有 Graphic 类所支持创建的。
解决办法是让 GraphicTool 通过拷贝或“克隆”一个 Graphic 子类的实例来创建新的 Graphic ,我们称这个实例为一个
原型
。GraphicTool 将它应该克隆和添加到文档中的原型作为参数。如果所有 Graphic 子类都支持 Clone 操作,那么 GraphicTool 可以克隆所有种类的 Graphic,如图。
因此在我们的音乐编辑器中,用于创建一个音乐对象的每一种工具都是一个用不同原型进行初始化的 GraphicTool 实例。通过克隆一个音乐对象的原型并将这个克隆添加到乐谱中,每个 GraphicTool 实例都会产生一个音乐对象。
我们甚至可以进一步使用 Prototype 模式来减少类的数目。我们使用不同的类来表示全音符和半音符,但可能不需要这么做。它们可以是使用不同位图和时延初始化的相同的类实例。一个创建全音符的工具就是这样的 GraphicTool,它的原型是一个被初始化成全音符的 MusicalNote。这样可以极大地减少系统中类的数目,同时也更易于在音乐编辑器中增加新的音符。
3、适用性
在下列情况下可以使用 Prototype 模式:
- 当一个系统应该独立于它的产品创建、构成和表示时。
- 当要实例化的类是在运行时指定时,例如,通过动态装载。
- 为了避免创建一个与产品类层次平行的工厂类层次时。
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
4、结构
5、参与者
Prototype(Graphic)
- 声明一个克隆自身的接口。
ConcretePrototype (Staff、WholeNote)
- 实现一个克隆自身的操作。
Client(GraphicTool)
- 让一个原型克隆自身从而创建一个新的对象。
6、协作
客户请求一个原型克隆自身。
7、效果
Prototype 有许多与 Abstract Factory 和 Builder 一样的效果:它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目。此外,这些模式使客户无须改变即可使用与特定应用相关的类。
下面列出 Prototype 模式的另外一些优点。
(1)运行时增加和删除产品
Prototype 允许值通过客户注册原型实例就将一个新的具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时建立和删除原型。
(2)改变值以指定新对象
高度动态的系统允许你通过对象组合定义新的行为——例如,通过一个对象变量指定值——并且不定义新的类。你通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。
这种设计使得用户无须编程即可定义新“类”。实际上,克隆一个原型类似于实例化一个类。Prototype 模式可以极大地减少系统所需要的类的数目。在我们的音乐编辑器中,一个 GraphicTool 类可以创建无数种音乐对象。
(3)改变结构以指定新对象
许多应用由部件和子部件来创建对象。例如电路设计编辑器就是由子电路来构造电路的。为方便起见,这样的应用通常允许你实例化复杂的、用户定义的结构,比方说,一次又一次地重复使用一个特定的子电路。
(4)减少类的构造
Factory Method 经常产生一个与产品类层次平行的 Creator 类层次。Prototype 模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象,因此你根本不需要 Creator 类层次。这一优点主要适用于像 C++ 这样不将类作为一级类对象的语言。
(5)用类动态配置应用
一些运行时环境允许你动态地将类装在到应用中。
一个希望创建动态载入类的实例的应用不能静态引用类的构造器,而应该由运行环境在载入时自动创建每个类的实例,并用原型管理器来注册这个实例。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。
Prototype 的主要缺陷时每一个 Prototype 的子类都必须实现 Clone 操作。
8、实现
因为在像 C++ 这样的静态语言中,类不是对象,并且运行时只能得到很少或得不到任何类型信息,所以 Prototype 特别有用。而在 C# 这样的语言中 Prototype 就是不那么重要了,因为这些语言提供了一个等价于原型的东西(即类对象)来创建每个类的实例。
当实现原型时,要考虑下面一些问题:
(1)使用一个原型管理器 当一个系统中原型数目不固定时(也就是说,它们可以动态地创建和销毁),要保持一个可用原型的注册表。客户不会自己来管理原型,但会在注册表中存储和检索原型。客户在克隆一个原型前会向注册表请求该原型。我们称这个注册表为原型管理器(prototype manager)。
原型管理器是一个关联存储器,它返回一个与给定关键字相匹配的原型。它有一些操作可以用来通过关键字注册原型和解除注册。客户可以在运行时更改甚至浏览这个注册表。这使得客户无需编写代码就可以扩展并得到系统清单。
(2)实现克隆操作 Prototype 模式最困难的部分在于正确实现 Clone 操作。当对象结构包含循环引用时,这尤为棘手。
克隆一个结构复杂的原型通常需要深拷贝,因为复制对象和原对象必须相互独立。
如果系统中的对象提供了 Save 和 Load 操作,那么你只需通过保存对象和立刻载入对象,就可以为 Clone 操作提供一个缺省实现。Save 操作将该对象保存在内存缓冲中,而 Load 则通过从该缓冲区中重构这个对象来创建一个副本。
(3)初始化克隆对象 当一些客户对克隆对象已经相当满意时,另一些客户将会希望使用他们所选择的一些值来初始化该对象的一些或是所有的内部状态。一般来说不可能在 Clone 操作中传递这些值,因为这些值的数目会由于原型的类的不同而有所不同。一些原型可能需要多个初始化参数,另一些可能什么也不要。在 Clone 操作中传递参数会破坏克隆接口的统一性。
可能会出现这样的情况,即原型的类已经为设定一些关键的状态值定义好了操作。如果这样的话,客户在克隆后马上就可以使用这些操作。否则,你就可能不得不引入一个 Initialize 操作,该操作使用初始化参数并据此设定克隆对象的内部状态。注意深拷贝 Clone 操作——一些副本在你重新初始化它们之前可能必须删除掉(删除可以显式地做也可以在 Initialize 内部做)。
9、代码示例
我们将定义 MazeFactory 的子类 MazePrototypeFactory 。该子类将使用它要创建的对象的原型来初始化,这样我们就不需要仅仅为了改变它所创建的墙壁或房间的类而生成子类了。
MazePrototypeFactory 用一个以原型为参数的构造器来扩充 MazeFactory 接口:
public classMazePrototypeFactory:MazeFactory
{privateMaze _maze;privateWall _wall;privateRoom _room;privateDoor _door;/// <summary> ///新的构造函数只初始化它的原型/// </summary> /// <param name="maze"></param> /// <param name="wall"></param> /// <param name="room"></param> /// <param name="door"></param> publicMazePrototypeFactory(Maze maze,
Wall wall,Room room,Door door)
{
_maze=maze;
_wall=wall;
_room=room;
_door=door;
}//用于创建晴碧、房间和门的成员函数是相似的:每个都要克隆一个原型,然后初始化 public overrideMaze MakeMaze()
{var maze =_maze.Clone();returnmaze;
}public overrideWall MakeWall()
{var wall =_wall.Clone();returnwall;
}public override Room MakeRoom(intnum)
{var room =_room.Clone();//还需要一个独立的操作来重新初始化内部状态 room.Initialize(num);returnroom;
}public overrideDoor MakeDoor(Room r1, Room r2)
{var door =_door.Clone();
door.Initialize(r1,r2);returndoor;
}
}
使用基本迷宫构件的原型进行初始化:
MazeGame mazeGame;var mazePrototypeFactory = new MazePrototypeFactory(newMaze(),new Wall(),new Room(),newDoor());
Maze maze= mazeGame.CreateMaze(mazePrototypeFactory);
为了改变迷宫的类型,我们用不同的原型集合来初始化 MazePrototypeFactory ,下面的调用用一个 BombedWall 和 RoomWithABomb 创建迷宫:
var bombedPrototypeFactory = new MazePrototypeFactory(newMaze(),new BombedWall(), new RoomWithABomb(), new Door());
Prototype 和 Abstract Factory 模式在某些方面是相互竞争的。但是它们也可以一起使用。Abstract Factory 可以存储一个被克隆的原型的集合,并且返回产品对象。