创建模式抽象了实例化过程。让系统独立于对象怎么创建,组合和实例化。类的创建模式用继承改变实例化的类。对象创建模式把实例化过程代理给另外一个对象。
当系统演变成依赖更多对象组合而不是类继承时,创建模式变得重要。因为此时,重点是把从硬编码一个固定行为集合改变成一个更小的能被组合成更复杂功能的行为集合。于是,更多的是需要用特殊行为创建对象而不是一个简单的类的实例。
这些模式中有两个反复出现的主题。其一,它们都封装关于系统使用拿个具体类的知识。其二,它们隐藏这些类的实例怎样创建和组合。所有的系统一般知道对象是他们抽象类定义好的的接口。相应的,创建模式在创建什么,谁创建,怎么创建,什么时候创建上面给你大量的灵活性。让你通过在结构和功能上广泛变化的“product”对象配置系统。配置可以是静态的(即编译阶段)也可以是动态的(在运行阶段)。
有时候创建者模式也是竞争关系。比如,有些场景,Prototype模式或Abstract Factory模式都很有益。有些时候,他们是互补的:Builder模式能使用其他的设计模式来实现哪个组件的构造。Prototype模式可以在实现时使用Singleton模式。
因为创建模式关系很近,我们将一起学习所有5个,从而强调他们的相似和不同。我们也将使用通用的例子--构建一个迷宫计算机游戏--来展示他们的实现。迷宫和游戏将轻微的从一个模式变到另一个模式。有时候游戏将是简单的找迷宫出口路径;这种情况玩家可能仅仅有一个本地迷宫的视角。有些时候迷宫包含要解决的问题和遭遇的危险,这些游戏将提供一个探险过的部分迷宫的地图。
我们将忽略掉什么将会在迷宫中和一个迷宫游戏有一个还是多个玩家这些细节。相反的,我们聚焦在迷宫怎么创建上面。我们定义一个迷宫为一个房间的集合。房间知道他们的邻居;很可能令居是其他房间,一堵墙,或通往另外房间的一扇门。
房间,门和墙这些类定义了所有我们例子中迷宫用到的组件。我们仅仅部分定义创建迷宫重要的这些类。忽略掉用于展示和闲逛的玩家和操作,以及一些和创建迷宫不相关的其他重要的功能。
下图用于展示这些类的关系:
每个房间有4个边。我们在C++实现的时候使用Direction枚举来指明房间的东南西北边:
enum Direction {North, South, East, West};
smalltaok 用相应的符号代表方向实现。
MapSite类是所有的迷宫组件的通用抽象类。为了简化例子,MapSite定义了一个操作,进入。它的意思取决于你将进入什么地方。如果你进入房间,就改变你的位置。如果你进入一个门,两件事的其一发生:如果门开着的,你就进入另一个房间;如果门关着的,你就碰壁了。
class MapSite {
public:
virtual void Enter() = 0;
}
Enter为更多复杂的游戏操作提供简单的基础。比如,如果你在一个房间然后说:“朝东走”,游戏可以简单的检测哪个MapSite当前在东边然后调用Enter。子类的Enter操作将支出是否你的位置改变或你碰壁了。在实际游戏中,Enter可以携带p移动的player对象作为参数。Room是MapSite的派生类,定义了迷宫组件之间的关键关系。她维护和其他MapSite对象的引用,以及存储一个房间号。房间号在迷宫中标识房间。
class Room : Public MapSite {
public:
Room(int roomNo);
MapSite * GetSide(Direction) const;
void SetSide(Direction, MapSite*);
virtual void Enter();
private:
MapSite * _sides[4];
int _roomNumber;
}
class Wall : public MapSite{
public:
Wall();
viertual void Enter();
}
class Door: public MapSite {
public:
Door(Room * = 0, Room * = 0);
virtural void Enter();
Room * OtherSideFrom(Room *);
private:
Room * _room1;
Room * _room2;
bool _isOpen;
}
我们需要知道更多除了迷宫的部分。我们将定义一个Maze类代表房间的集合。Maze也可以通过使用RoomNo操作查找特定房间。
class Maze{
public:
Maze();
void AddRoom(Room *);
Room * RoomNo(int) const;
private:
// ......
}
RoomNo可以使用线性查找,hash table,或者甚至一个简单的数组来做查找。但是这里我们不必担心这些细节。相反,我们将聚焦在怎样指定一个maze对象的各种组件。
我能定义的另一个类是MazeGame,在这个类中创建maze。一个直接的方式就是用一系列添加组件到maze然后连接他们的方式创建maze。比如下面的函数将创建一个maze,有两个房间,房间之间有个门:
Maze* MazeGame::CreateMaze () {
Maze* aMaze = new Maze;
Room* r1 = new Room(1);
Room* r2 = new Room(2);
Door* theDoor = new Door(r1, r2);
aMaze->AddRoom(r1);
aMaze->AddRoom(r2);
r1->SetSide(North, new Wall);
r1->SetSide(East, theDoor);
r1->SetSide(South, new Wall);
r1->SetSide(West, new Wall);
r2->SetSide(North, new Wall);
r2->SetSide(East, new Wall);
r2->SetSide(South, new Wall);
r2->SetSide(West, theDoor);
return aMaze;
}
这个功能很完备,考虑到所有创建带两个房间迷宫需要的操作。当然,有办法让它更简单。比如,房间构造应该先用墙初始化边。当时这样仅仅是把代码移到别的地方。真正的问题是这些函数的规模,而是他的固定性。硬编码了迷宫的布局。改变布局意味着改变这些成员函数,要么是重写他们--意味着重新实现整个事情--要么是改变部分来适应--意味着容易出错并且不能重用。
创建模式向你展示怎样让这个设计更灵活,不一定更小。它将很容易修改定义迷宫组件的类。
假设你想为一个包含(所有)施了魔法迷宫的新游戏重用一个已存在的迷宫布局。这个施了魔法的迷宫游戏有新类型的组件,比如需要咒语的门,门上了锁,随后只能被一个咒语打开。施了魔法的房间,有非常规的项目在里面,比如魔法钥匙或咒语。你怎样轻松地改变迷宫创建,从而用这些新类和对象创建迷宫。
在这种情况下,要改变的最大的障碍是位于这些类中实例化的硬编码。创建模式提供了不同的方式把明确的引用移动到需要实例化他们的编码的具体类中:
- 如果创建迷宫调用虚函数而不是构造函数去创建所需要的房间,墙和门,那么你可以通过使MazeGame的重新定义虚函数的子类实例化从而改变类获取。这是抽象工厂方法的一个实现方法。
- 如果创建迷宫被传入了一个对象作为参数,用于创建房间,墙和门,然后你可以通过不同的参数改变房间,墙和门的类。这是一个抽象工厂模式的例子。
- 如果创建迷宫传入了一个使用为添加房间,们和墙的操作能够创建新的迷宫对象,然后你能通过继承改变部分迷宫或构建迷宫的方式。这就是一个构建模式的例子。
- 如果创建迷宫是被各种原类型的房间,们和墙对象参数化,这些对象被拷贝和添加到迷宫中,然后你可以通过用不同的原型对象替换这些原型对象改变迷宫构成。这是一个原型模式的例子。
剩下的创建模式,单例,能够保证每次游戏只有一个迷宫,所有的游戏对象准备访问它。不用求助于全局对象或者函数。单例也很容易扩展或替换迷宫,不需要涉及已存在的代码。