设计模式学习整理之创建型模式
概述
定义----通过抽象实例化的过程,帮助一个系统独立于如何创建、组合和表示它的那些对象。
创建型模式属于对象创建模型。所谓对象创建模型就是说将实例化的工作委托给另一个对象来做。与之相对应的是类创建模型,这是一种通过继承改变被实例化的类。
创建型模式有两个重要的特点:
1) 客户不知道创建的具体类是什么(除非看源代码)
2) 隐藏了类的实例是如何被创建和放在一起的
这两个重要的特点是通过抽象类的虚接口技术做到的,这样设计者可以决定何时、何地、如何创建和由谁来创建。
迷宫模型,见下图
![](https://p-blog.csdn.net/images/p_blog_csdn_net/sheismylife/dcce43da648b4ebd9d122bb883f996d2.png)
MapSite是所有迷宫中基本构件的基类,提供了一个纯虚函数Enter
Maze是Romm的集合类。
MazeGame::CreateMazeNormal()方法根据需求创建了迷宫的一切,并把它们组合起来,这是我们最常见的面向对象思维。
Maze* CreateMazeNormal()
{
Maze* pmaze=new Maze();
Room* pr1=new Room(1);
Room* pr2=new Room(2);
Door* pdoor=new Door(pr1,pr2);
pr1->SetSide(North,new Wall);
pr1->SetSide(East,pdoor);
pr1->SetSide(South,new Wall);
pr1->SetSide(West,new Wall);
pr2->SetSide(North,new Wall);
pr2->SetSide(East,new Wall);
pr2->SetSide(South,new Wall);
pr2->SetSide(West,pdoor);
}
但是这样的做法无法面向未来的变化。如果迷宫中增加了一个新的需要念咒语才能打开的门,一个装了神奇宝贝的房间,结果就是CreateMazeNormal需要重新修改以适应现在的新变化。因此这样的代码没有可重用性,当重新修改的时候,还会引发错误,导致维护困难。
创建型模式可以帮助我们无需改变创建代码,就可以轻松的增加各种迷宫的构件。(但是,并不是所有的情况都一定要采用这种做法,应该根据情况而定。)
Abstract Factory
![](https://p-blog.csdn.net/images/p_blog_csdn_net/sheismylife/07cbd5dfdbdd4027a1ed513e503b1d6f.png)
除了创建型模型带来的优点是使用Abstract Factory的理由以外,该模式还有两个特点:
1) 当一个系统由多个产品系列组成,并且需要使用其中的某一个时
如上图,Product系统有两个产品系列,Product1和Product2,ConcreateFactory1::CreateProductX( )创建出来的对象将会使用Product1系列。这使得交换产品系列很方面,只需要实例化不同的ConcreateFactory即可。
2)增强了某些类之间的关联,比如ProductA1和ProductB1都属于同一个产品系列,互相之间可能需要协同工作,有利于保证产品的一致性。
缺点:
由于AbstractFactory类提供了针对不同系列的统一创建接口,如果新的产品要加入到产品系列中,就需要增加创建虚函数。比如我们现在有一个ProductC产品,我们需要增加类AbstractProductC,增加AbstractFactory::CreanteProductC方法,并且两个产品系列的实际创建者ConCreateFactory1、ConCreateFactor2都要实现该方法。
可以通过给方法加参数的方式来指明创建的是什么产品,这样客户代码就无需改变,只要传递不同的参数。AbstractFactory类只需要提供一个CreateProduct(const string& name)方法即可。
AbstractFactory的实现:
如果从头至尾只需要一个该类的对象,Singleton是一个好方法,比如
CMyAbstractFactory* pfactory=CMyAbstractFactory::Instance ( paramerter);
pfactory->CreateProductA( );
也可以用派生类的方法,如上图所示的ConCreateFactory1.这种方法的特点是需要为每一个系列都创建一个派生类。
也可以使用Prototype来处理产品系列较多的情况。
现在我们用Abstractory Pattern优化创建迷宫的设计,我们的迷宫可能会有三个系列:普通迷宫、带魔法的迷宫和有炸弹的迷宫
![](https://p-blog.csdn.net/images/p_blog_csdn_net/sheismylife/f1019320dd57429bb698d38278612a71.png)
使用MazeFactory将可以创建普通迷宫,使用EnchantedMazeFactory创建魔法迷宫,使用BomedMazeFactory用来创建带炸弹的迷宫
三种不同的迷宫。
CreateMaze函数内部创建代码依赖于参数factory,用户可以将子类对象传递进来。该函数内部无需改变,只需要传递不同的创建对象即可。
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;
}
Builder
Abstract Factory的引入使得CreateMaze无需修改,但是这只是在迷宫系列切换的时候,并且每个迷宫里面的内部表示(用什么类、几个类表达房间、墙壁、门等)都一样。如果房间不再用Room类表达,而是换用其他类构建,CreateMaze函数还是要进行大幅度修改。原因是创建迷宫的算法和迷宫内部表示(那些类表示房间、门、墙壁等)紧密关联。这样要想在大幅度修改创建算法(通常可能由客户编写)的情况下,创建新的调整后的迷宫几乎是不可能的事情。
Builder能够将创建算法和对象内部表示分离开来。
![](https://p-blog.csdn.net/images/p_blog_csdn_net/sheismylife/c9bff79b4fbd48859316f43e1b61a265.png)
Product-----------我们要创建的产品
Builder------------提供抽象接口创建Product,如果Product够复杂,比如迷宫,Builder或许应该提供的是多个创建迷宫不同部分的虚函数
ConcreteBuilder------------真正创建Product的对象,实现Builder的抽象接口,并且提供一个方法,让客户获取创建好的Product对象
Director-----用户不直接使用Builder,而是通过Director对象调用Builder的虚函数来创建Product,用户只调用Director的方法,这样对用户又隐藏了如何通过Builder虚函数创建的算法。
这样用户调用代码应该如下:
ConcreateBuilder * pBuilder=new ConcreateBuilder;
Director director;
director.Construct(pBuilder);
Product* pProduct=pBuider->GetResult( );
如果这时候Product的内部表示改变了,我们只需要修改原来的ConCreateBuilder类或者创建一个新的ConCreateBuilder2类,抽象接口不变,外部客户调用代码就无需改变。
我们也可以定义另一个Director2类,这个Director2类可能会创建出新的Product2对象。
Builder模式不是一下子就创建出对象的,创建过程经过了Director和ConcreateBuilder对象,使得我们可以对构造过程进行更精细的控制。
现在我们来看看在迷宫中Builder的表现:
class MazeBuilder //等价于Builder,但是不是纯虚类,因为我们希望它能够提供默认的构造方法
{
public:
virtual void BuildMaze() { }
virtual void BuildRoom(int room) { }
virtual void BuildDoor(int roomFrom, int roomTo) { }
virtual Maze* GetMaze() { return 0; }
protected:
MazeBuilder();
};
这三个虚函数隐藏了Maze、Room、Door等迷宫的内部表示类,因此实现了构造过程和复杂对象内部表现的分离。
Maze* MazeGame::CreateMaze (MazeBuilder& builder)
{
builder.BuildMaze();
builder.BuildRoom(1);
builder.BuildRoom(2);
builder.BuildDoor(1, 2);
return builder.GetMaze();
};
MazeGame类扮演了Director的角色,CreateMaze通过调用虚函数来创建复杂对象,他无须了解复杂对象的内部表示。也许CreateMaze随着需求的变化可能会创建拥有更多房间的迷宫,但是只是通过调用虚函数来完成,如下:
Maze* MazeGame::CreateComplexMaze (MazeBuilder& builder) {
builder.BuildRoom(1);
// ...
builder.BuildRoom(1001);
return builder.GetMaze();
}
如果MazeBuilder提供的虚函数不能满足复杂对象的内部表示,我们可以创建子类来扮演ConcreateBuilder角色,重载三个虚函数,将复杂对象的内部表示封装起来。
客户调用代码可能如下:
MazeGame game;
MyMazeBuilder builder;
Maze* pMaze=game.CreateMaze(builder);
builder.GetCounts(rooms, doors);
重点:
何谓对象的内部表示?
我认为这是指复杂对象内部的基本构件对象是否发生变化,比如Room类是否变成了其他的类的组合,可以用对象静态结构图是否发生变化来判断。
如果没有发生变化,只是Room对象增加或者减少了,则不能称为内部表示发生变化。
Factory Method
创建者提供虚函数作为创建产品的抽象接口。具体创建什么产品由创建者的派生类决定。
![](https://p-blog.csdn.net/images/p_blog_csdn_net/sheismylife/638ccee7f75e4632a78bdda0a084690b.png)
如果AbstractFactory采用由派生工厂类来创建某个系列中的产品时,它实际上就是内部采用Factory Method模式。
Factory Method的几个注意点:
1) 创建函数可以是虚函数也可以是纯虚函数,虚函数提供了默认实现
2) 创建函数可以接收参数来决定创建什么产品
3)Factory Method容易导致创建过多的Creator的子类以对应不同的产品,这个方法可以通过模板技术来解决
class Product
{
public:
virtual ~ Product( ){};
...
};
class MyProduct:public Product
{
public:
MyProduct(){};
};
class Creator
{
public:
virtual Product* CreateProduct()=0;
};
template<class TheProduct>
class StandardCreator :public Creator
{
public:
virtual Product* CreateProduct( );
};
template<class TheProduct>
Product* StandardCreator<TheProduct>::CreateProduct()
{
return new TheProduct;
}
客户调用代码:
StandardCreator<MyProduct> myCreator;
Product* p=myCreator.CreateProduct();
这样,迷宫的创建代码将会如下:(感觉没有Builder好)
class MazeGame {
public:
Maze* CreateMaze();
// factory methods:
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); }
};
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;
}
class BombedMazeGame : public MazeGame {
public:
BombedMazeGame();
virtual Wall* MakeWall() const
{ return new BombedWall; }
virtual Room* MakeRoom(int n) const
{ return new RoomWithABomb(n); }
};
class EnchantedMazeGame : public MazeGame {
public:
EnchantedMazeGame();
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;
};
Prototype
![](https://p-blog.csdn.net/images/p_blog_csdn_net/sheismylife/9767a24f6e804101a9c0ae875105f950.png)
Factory Method会导致创建类与产品类产生平行结构,有时候这样很不错,但是当产品类越来越多时,为每个产品类定制一个创建类是一件苦恼的事情,有时候也是不可能的事情,因为有可能创建逻辑甚至都不知道具体是什么产品类,比如说动态创建一个由客户添加进来的产品。
Prototyp模式要求产品类必须支持基类的Clone操作,创建逻辑只需要调用需函数,不需要知道具体是什么产品类,这样就允许动态创建,并且解决了平行结构不适应大量产品类情况的问题。
缺点:并不是所有的产品子类都能轻松的支持Clone操作,比如如果产品内部有循环引用的对象时。
在动态创建和销毁的运用中,建议使用一个原型管理器。该管理器提供注册和注销操作,可以减需要的Key和Value(原型)保存在这里,这样加载代码酒依赖于管理器中有多少原型,这样的设计有足够的弹性。
Clone通常应该是深拷贝。
Singleton
Singleton模式通常用于只需要一个对象生存的场合,但是这句话不是Singleton的全部意义,模式不是公式,它是可以变化的。比如:一个系统打印对象,进程内只需要一个,但是一个多线程并发访问数据库的程序,每个线程可能都需要一个连接对象,这样Singleton就意味着进程内有多个,每个线程里面有一个。但是如果允许连接多个数据库呢?很有可能就变成了每个线程内只允许有一个连着某个特定数据库的连接对象。
Singleton的实现者需要提供一个全局的访问点给用户。最简单的就是静态成员函数Instance,为什么不用全局变量,因为全局变量不能阻止别人创建同类型的变量,而且也污染了全局空间(别人不能和你用一样的变量明)。所以我们要把类的构造函数变为protected。
一个对象内部可以保存一个静态指针,然后在Instance函数内部实例化,并返回它。这种解决方案可以解决刚才打印机的要求。
一个对象可以保存一个静态map,map每一项保存线程ID和静态对象指针,并且提供维护该map的一系列方法,这样就可以解决每个线程需要有一个对象的要求。
但是这样就够了么,我还碰到一个不同的需求,要求运行时决定创建不同的对象。这时候可以在Instance函数上加上参数,通过参数来创建不同的对象。这些对象也可以派生自父Singleton类。
也许每个线程允许对象数目不能超过3个,没关系,我们可以在Instance内部实现这些控制逻辑。
我想表达的是,Singleton可以有很多变种,有时候甚至让你感觉名不副实,但是这就是设计模式的魅力。我也是从一开始的教条主义到能接受很多精彩的变化,有时候甚至都不应该用它,或许很多是多态就够了。
由于Singlton很简单,所以就不再详细分析在迷宫中的应用了。