設想有這麼一個問題,在一個迷宮遊戲中,首先需要的是迷宮地形結構(地圖),迷宮地圖中包含了若幹個格子,每個格子代表一個房間,然後每個房間四周可以有牆、有門等,現在要求用程式建立這麼一個地圖。
為了更好地了解迷宮的地圖,看一張迷宮結構的關系圖:

Maze代表迷宮類,裡面裡面有一些方法,比如AddRoom()方法,為迷宮添加房間;比如RoomNo()方法,根據房間号去查找對應的房間等等。
MapSite是迷宮組成元素的一個基類,下面有三個繼承類分别是:Room(房間)、Wall(牆)、Door(門)。
要建構這麼一個Maze地圖,最簡單粗暴的方法就是地圖建立的需要,直接用寫死的方式(比如用new 加類名建立一個對象)将每個Maze組成元素建立出來,并且添加到Maze當中去。
具體實作如下:
</pre><pre>
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;
}
這樣雖然能夠實作這個功能,但是玩過迷宮類似的遊戲就知道,有些不同房間之間的風格是不一樣的(比如背景、比如一些門可以被炸開等等),這就意味這需要需要用不同的類來表不同風格的同一個Maze元素,比如可能有Room0, Room1, Room2分别從積累Room繼承而來,代表三種不同風格的房間。這樣一來,如果要改變Maze的風格,那所有建立Maze元素的地方都需要跟着改變,相當于牽一發而動全身了,代碼的靈活性不夠好,而且還在改的過程中極其麻煩還容易出錯。
在這種背景下,Creational Patterns(建立模式)就有勇武之地了,它是一種将類的執行個體化過程抽象出來的一種方法。建立模式包括5種具體的模式:Abstract Factory(抽象工廠模式), Builder(建立者模式,生成器模式), Factory Method(工廠方法模式), Prototype(原型模式), Singleton(單例模式)。
Abstract Factory:
Abstract Factory提供了一個抽象的接口,通過這個接口可以建立具有相同風格的一類對象,在建立的過程中不需要知道正在建立的具體是哪一種風格。這樣的話,無論具體風格類怎麼改,不管具體風格類是什麼名字,在執行個體化的過程中,都隻需要用Abstract Factory提供的統一的抽象接口去實作。
Abstract Factory的具體結構如下:
Abstract Factory作為父類,提供一個抽象的接口供Client調用執行個體化,ConcreteFactory1和ConcreateFactory2都繼承于AbstractFactory,代表兩個具體的風格類,這些具體的風格類中實作了各自風格的Product。具體來說,Client有兩類抽象的類指針,分别是AbstractFactory(Factory指針)和AbstractProduct(Product指針,每個Product對應一個這樣的抽象指針),Client調用AbstractFactory中的CreateProduct,CreateProduct生成具體風格的Product并傳回,然後Client就用AbstractProduct指向傳回的這個生成并傳回的Product,這樣就完成了了Product的建立。具體到之前講的Maze地圖,Product就可以代表Maze的組成元素,比如Room、Wall、Door。
那麼通過Abstract Factory模式,就可以這樣來建立Maze地圖:
class MazeFactory
{
public:
MazeFactory();
virtual Maze* MakeMaze() const
{ return new Maze; }
virtual Wall* MakeWall() const
{ return new Wall; }
virtual Room* MakeRoom(int n) const
{ return new Room(n); }
virtual Door* MakeDoor(Room* r1, Room* r2) const
{ return new Door(r1, r2); }
};
class EnchantedMazeFactory : public MazeFactory
{
public:
EnchantedMazeFactory();
virtual Room* MakeRoom(int n) const
{ return new EnchantedRoom(n, CastSpell()); }
virtual Door* MakeDoor(Room* r1, Room* r2) const
{ return new DoorNeedingSpell(r1, r2); }
protected:
Spell* CastSpell() const;
};<pre name="code" class="cpp">Maze* MazeGame::CreateMaze (MazeFactory& factory)
{
Maze* aMaze = factory.MakeMaze();
Room* r1 = factory.MakeRoom(1);
Room* r2 = factory.MakeRoom(2);
Door* aDoor = factory.MakeDoor(r1, r2);
aMaze->AddRoom(r1);
aMaze->AddRoom(r2);
r1->SetSide(North, factory.MakeWall());
r1->SetSide(East, aDoor);
r1->SetSide(South, factory.MakeWall());
r1->SetSide(West, factory.MakeWall());
r2->SetSide(North, factory.MakeWall());
r2->SetSide(East, factory.MakeWall());
r2->SetSide(South, factory.MakeWall());
r2->SetSide(West, aDoor);
return aMaze;
}
MazeGame game;
BombedMazeFactory factory;
game.CreateMaze(factory);
BombedMazeFactory是繼承于MazeFactory的一個具體類,可以代表一類具體風格的Maze。将BombedMazeFactory一個執行個體對象傳入CreateMaze中,CreateMaze通過一個抽象的MazeFactory引用去生成各種Maze的組成元素,這樣一來就把Maze組成元素的具體建立過程封裝了,而且在建立的時候也不知道具體建立了哪一類Maze。
Builder:
書上是這樣解釋的,通過Builder(生成器模式)這種模式,可以将複雜對象的建立和顯示分離,這樣同一個建立過程可以建立不同的顯示。感覺可以更通俗點了解:首先确定要建立什麼,确定之後每次把要建立的資訊發送給Builder,Builder自動幫你建立好并添加到建立結果,當所有建立過程完成之後, 就可以通過Builder對象去通路最終建立結果。而具體怎麼建立,怎麼把新建立的資訊添加到現有結果中,這些過程都不需要關心。
Builder的具體結構如下:
上圖中,Director首先确定要建立什麼,确定要建立什麼之後,對與每個需要建立的資訊,都通過一個抽象的Builder指針調用一個BuilderPart()函數去建立,這個時候,Director方面就不需要管Builder具體是怎麼建立并把建立結果添加到現有的建立結果中。當然對于不同的建立風格,還可以有不同的Builder類,比如上圖中,Builder隻是一個基類,可以通過繼承的方式建立出它的多個具體的子類,在子類中可以根據自己不同的風格重新實作具體的建立、組成方法。當整個過程建立完之後,就可以通過抽象的Builder指針去得到一個完整的Product。或許,在某些情況下,在Product建立完成之後,需要知道中間某些過程中的建立情況,這種情況的話, 就需要用一個特殊的資料結構來維護Product,比如可以簡單地用一棵樹來儲存Product各個建立階段的具體建立情況,不同節點代表不同建立階段的建立情況。具體到之前說的迷宮例子,這裡的Product就代表Maze地圖。
那麼通過Builder模式,可以這樣來建立Maze地圖:
class MazeBuilder
{
public:
virtual void BuildMaze() { }
virtual void BuildRoom(int room) { }
virtual void BuildDoor(int roomFrom, int roomTo) { }
virtual Maze* GetMaze() { return 0; }
protected:
MazeBuilder();
};
class StandardMazeBuilder : public MazeBuilder
{
public:
StandardMazeBuilder();
virtual void BuildMaze();
virtual void BuildRoom(int);
virtual void BuildDoor(int, int);
virtual Maze* GetMaze();
private:
Direction CommonWall(Room*, Room*);
Maze* _currentMaze;
};
Maze* MazeGame::CreateMaze (MazeBuilder& builder)
{
builder.BuildMaze();
builder.BuildRoom(1);
builder.BuildRoom(2);
builder.BuildDoor(1, 2);
return builder.GetMaze();
}
Maze* maze;
MazeGame game;
StandardMazeBuilder builder;
game.CreateMaze(builder);
maze = builder.GetMaze();
StandardMazeBuilder是MazeBuilder的一個子類,代表一類建立風格,在這個子類裡面,根據類本身的特點重新實作了各種Build方法。首先,将MazeBuillder的一個具體的子類StandardMazeBuilder的一個對象傳入CreateMaze中,在CreateMaze中就可以通過一個抽象的MazeBuilder引用,調用各種建立方法去建立Maze中的組成元素,建立完成以後,可以通過抽象的MazeBuilder引用去獲得最終建立結果。
Factory Method:
Factory Method(工廠方法模式)定義了一個抽象的接口用于建立對象,但是讓具體的類去決定對哪一個類進行執行個體化,執行個體化後并傳回結果。運用Factory Method,首先建立一個基類,提供了抽象的建立Maze組成元素的接口,之後可以派生出具體不同的子類,每個子類對Maze中的抽象接口根據類本身的特點重新實作。
Factory Method的具體結構如下:
上圖中,Creator作為一個基類,提供了一個抽象的方法FactoryMethod()用于建立Product,在Creator的下面可以派生出各種不同的子類,各個基類可以根據類本身特征重新實作一些抽象接口。具體到我們之前講的例子,這裡的Product可以是Maze裡面的組成元素。
那麼通過Factory Method模式,可以這樣來建立Maze地圖:
</pre><pre>
class BombedMazeGame : public MazeGame
{
public:
BombedMazeGame();
virtual Wall* MakeWall() const
{ return new BombedWall; }
virtual Room* MakeRoom(int n) const
{ return new RoomWithABomb(n); }
};
Maze* MazeGame::CreateMaze ()
{
Maze* aMaze = MakeMaze();
Room* r1 = MakeRoom(1);
Room* r2 = MakeRoom(2);
Door* theDoor = MakeDoor(r1, r2);
aMaze->AddRoom(r1);
aMaze->AddRoom(r2);
r1->SetSide(North, MakeWall());
r1->SetSide(East, theDoor);
r1->SetSide(South, MakeWall());
r1->SetSide(West, MakeWall());
r2->SetSide(North, MakeWall());
r2->SetSide(East, MakeWall());
r2->SetSide(South, MakeWall());
r2->SetSide(West, theDoor);
return aMaze;
}
MazeGame作為一個抽象的Creator, 提供MakeRoom(), MakeDoor(), MakeWall()的抽象接口作為Factory Method。BombedMazeGame繼承于MazeGame,對Factory Method具體實作了。這裡容易和Abstract Factory模式混淆,在Abstract Factory模式中,Make方法單獨地在一個Factory類裡面,而在Factory Method模式中,Make方法直接放到Game類裡面去了。
Prototype:
運用Prototype(原型模式)這個模式進行執行個體化時,不是我們之前講的通過建立一個對象的方式,而是直接複制原型執行個體進行實作。
Prototype的具體結構如下:
上圖中,Prototype作為一個基類,提供一個抽象的Clone()接口,用于Copy執行個體對象。下面派生出來的兩個具體的Prototype根據各自特點重新實作Clone()接口。Prototype模式中,最重要的就是Clone()接口的實作,拷貝的時候要注意是用深拷貝還是淺拷貝,有的時候還需要考慮循環引用的問題。具體到之前說的Maze地圖例子,可以為每一個Maze組成元素提供一個抽象的Prototype類,然後根據Maze的不同風格去派生出不同的具體子類。
那麼通過Prototype模式,可以這樣來建立Maze地圖:
class MazePrototypeFactory : public MazeFactory
{
public:
MazePrototypeFactory(Maze*, Wall*, Room*, Door*);
virtual Maze* MakeMaze() const;
virtual Room* MakeRoom(int) const;
virtual Wall* MakeWall() const;
virtual Door* MakeDoor(Room*, Room*) const;
private:
Maze* _prototypeMaze;
Room* _prototypeRoom;
Wall* _prototypeWall;
Door* _prototypeDoor;
};
MazeGame game;
MazePrototypeFactory simpleMazeFactory(
new Maze, new Wall, new Room, new Door
);
Maze* maze = game.CreateMaze(simpleMazeFactory);
在MazePrototypeFactory類中,關鍵在于四個prototype指針,對應的四個Make函數通過對應的prototype指針去Clone執行個體,比如MakeRoom():
Room* MazePrototypeFactory::MakeRoom () const
{
return _prototypeRoom->Clone();
}
Clone是Prototype模式的最關鍵部分,它是在Maze組成元素類當中實作的,比如Wall的Clone函數的實作:
Door* Door::Clone () const
{
return new Door(*this);
}
這樣實作Clone函數,還要依賴于被Clone的類的拷貝構造函數,也就是說每個被Clone的類需要實作自身的拷貝構造函數。
Singleton:
Singleton(單例模式)模式確定每個類隻有一個執行個體,并提供一個全局通路的指針。
Singleton模式的具體結構如下:
在Singleton模式中,兩個關鍵部分在于一個指向唯一執行個體對象的指針,一個傳回唯一執行個體對象的函數。具體應用中,需要将Singleton的構造函數放到Protected中,以防被外部意外建立執行個體,進而就破壞了Singleton隻有一個執行個體的性質。
具體到之前講的Maze地圖的建造,可以實作如下:
class MazeGame
{
public:
Maze* CreateMaze();
// factory methods:</span>
virtual Maze* MakeMaze() const
{ return new Maze; }
virtual Room* MakeRoom(int n) const
{ return new Room(n); }
virtual Wall* MakeWall() const
{ return new Wall; }
virtual Door* MakeDoor(Room* r1, Room* r2) const
{ return new Door(r1, r2); }
};
對于Singleton模式的實作,也有一些具體的技巧,比如通過注冊Singleton,在以後建立執行個體的時候可以直接根據類的的代号(可以是名字,也可以是編号)來調用具體的類來建立執行個體。
五種Creational模式各有優劣勢,在具體應用中具體情況具體分析選取合适的模式很重要。
參考文獻:
[1] Erich Gamma, Richard Helm,Ralph Johnson,John Vlissides. Design Patterns: Elements of Reusable Object Oriented Software.Pearson Education.