漫谈设计模式 —— 创建行为的封装

在经典的24个设计模式中,有一部分是关于对创建行为的封装的,他们分别是原型模式、单例模式、生成器模式、简单工厂模式、工厂方法模式、抽象工厂模式。他们都是致力于把实例化对象的职责解耦出来,通过不同的形式封装达到不同的效果。我们从最简单的例子开始入手看看这几种模式的相似与不同。

通常,我们实例化一个对象最简单的就是通过new操作符直接创建,但如果该对象比较复杂,在创建的同时要进行许多初始化工作,例如游戏中创建一个NPC对象,在new一个NPC后,要把他添加到相应的场景中,还有要对他设置许多属性,还要根据他身上的装备来绑定各种部件。通常我们会写一个NPCFactory类,提供一个NPCFactory::CreateNPC()接口把这一连串创建NPC相关的流程封装起来。在设计模式中把这样的做法叫做简单工厂(Simple Factory)模式简单工厂严格来说不算是一个完整的设计模式,但很多人却误以为这就是所谓的工厂模式。

还有一个与上面说的很相似的是生成器(Builder)模式,不同点在于生成器模式把创建过程分开几个步骤封装到类中。就像这样:

NPCFactory.Init(szResourcePath);
NPCFactory.AddToScene(nSceneID);
NPCFactory.SetProperty();
NPCFactory.Equit();
NPC* pNpcA = NPCFactory.GetObject();

生成器模式的最大特点是分步骤,他把创建的过程封装成好几个步骤并交由使用者控制,使用者可以更灵活的控制创建,但相对应他的问题也是使用者必须熟悉创建流程与规则,要知道先Init、再设置、最后调GetObject才真正得到对象。

在介绍完简单工厂与生成器模式后,我们接着来看看另外两个很相似的工厂模式。工厂方法(Factory Method)模式抽象工厂(Abstract Factory)模式

首先来看看工厂方法(Factory Method)模式。工厂方法模式的思想就是把创建的方法定义为抽象的基类方法,然后创建的具体实现交由派生类决定,而基类通过抽象的接口脱离创建实现定义其他行为。

举个例子,我们现在有两种NPC,HuntNPC具有狩猎功能,GuideNPC具有引导玩家功能。然后我们有两种场景,城市和是野外。城市场景要创建GuideNPC引导玩家,野外场景要创建HuntNPC狩猎怪物。场景都有维护NPC的容器,并且在Active中遍历场景内的NPC,调用他们的Partol方法。工厂方法模式组织的类图如下:

FactoryMethod

Scene通过抽象的CreateNPC接口把不同场景创建不同NPC,以及不同NPC的不同初始化细节都交由子类实现,这就是工厂方法的精髓。其实工厂方法模式与模板方法模式是十分相似的,都是把变化的封装成抽象接口交由子类实现,而基类则把不变的流程(算法)固定下来。不过工厂方法模式更进一步的指明是把创建行为抽象成接口由子类实现。

假设我们现在有更复杂的需求,场景里面不仅有NPC,还有树。而且树也分两种,野外场景的树是虚的,仅仅只有表现功能,不会阻碍玩家的行动,城市场景中的树则是实的,会挡着某些路或围起某些建筑不让玩家靠近。不仅如此,野外场景创建时还要创建怪物,而城市场景在创建时则要创建城市内的交通。在这种创建时需要创建一群相关或依赖对象时,抽象工厂(Abstract Factory)模式就派上用场了。先从UML图直观认识一下:

AbstractFactory

我们把创建的职责单独出来封装到SceneFactory中,场景拥有SceneFactory对象并利用它进行场景的实例化。而SceneFactory则通过派生不同的实例来完成不同场景的创建细节。TownFactory负责城市的创建,实例化引导型NPC、实体的树以及交通。WildernessFactory负责野外的创建,实例化狩猎型NPC、虚拟的树以及怪物。抽象工厂模式的精髓之处在于把工厂通过组合形式抽离出来,并且派生不同的实例完成不同的创建。这样做不仅把创建职责解耦,并且可以在运行时改变。例如某个场景在周末活动的时候会变成野外类型,而平常则是城市类型,这样只需要运行时动态改变SceneFactory对象,就可以利用不同的SceneFactory实现不同类型的初始化。

上面介绍的简单工厂模式,生成器模式,工厂方法模式与抽象工厂模式都十分相似,他们都是致力于把初始化解耦,有的通过组合,有的通过继承。他们都很容易混淆,但我们其实不需要把他们区分得那么清楚,按照自己的需求使用即可。

还有一些关于封装创建行为的模式,其中一个就是鼎鼎大名的单例(Singleton)模式。关于单例模式相信大家都不会陌生,它是通过把构造跟析构函数私有化,让外界不能直接通过new操作符创建实例,然后自己提供一个GetInstance()接口把全局唯一的实例返回给使用者。值得注意的一点是,我们实现单例时通常只是把构造跟析构函数定义为私有,但C++编译器会帮我们实现一个按位拷贝的复制构造函数和赋值操作,使用者利用这个特性还是可以把对象拷贝,从而破坏单例的唯一性。所以如果要防止这样的捣乱行为,还是应该注意把复制构造函数与赋值操作符也显式定义并声明为私有。

最后一个关于封装创建行为的模式是原型(Prototype)模式。原型模式通过复制现有实例来创建新的实例,使用者甚至不需要知道该对象创建过程与细节。这是个理论上很强大的模式,但在日常C++开发中几乎没有看到。因为如果原对象带有指针的成员数据,那么按位拷贝出来的新对象与旧对象则共同引用着那些指针指向的数据,操作时会导致各种问题。除了指针以外,还有一些不可公用的数据如角色ID,在容器中的下标等等,这些数据直接拷贝使用也会带来各种问题。所以通过直接拷贝来产生新对象的原型模式,在C++程序开发中很少使用。

本篇介绍了经典设计模式中关于创建行为封装的6个模式,他们包括4个很相似的模式:简单工厂模式、生成器模式、工厂方法模式以及抽象工厂模式,还有一个最常见的单例模式以及一个非常少见的原型模式。通过举例分析介绍了他们之间的相似与不同,希望对大家有所帮助。下一篇文章让我们接着谈谈其他设计模式。

Tagged , , , , , , . Bookmark the permalink.

Comments are closed.