漫谈设计模式 —— 结构的封装

上一篇文章谈了一些对创建行为封装的模式,还有一些模式他们是在结构层面来封装的,分别有外观模式、适配器模式、代理模式、装饰者模式、组合模式、桥梁模式与蝇量模式。什么是在结构层面封装呢?我们可以先简单的理解为在一个原有的类上面再包了一个类。那为什么要这样做呢?这样做可以达到很多灵活的变化,我们首先来看最直观也是最简单的,就是改变接口。

用一个类包裹一个原有的类,从而改变它的接口,这就是适配器(Adapter)模式。举个简单的例子,假设我们游戏中特效管理器已经有一个在场景内指定坐标播放特效的接口SFXMgr::PlaySceneSFX(int nX, int nY, int nZ)。现在我们有一个新需求是通过D3DVECTOR传递坐标,那我们第一个想法就是重载PlaySceneSFX,提供SFXMgr::PlaySceneSFX(const D3DVECTOR& vPosition)接口。如果再来一个需求,是在场景内一个物件脚下播放特效,这样我们又得重载一个SFXMgr::PlaySceneSFX(const SceneObject* pObject)接口。这样做不但把特效管理器改乱了,而且特效管理器还要看见D3DVECTOR和SceneObject。为了解决这样的问题我们可以写一个SFXMgrAdapter包裹着SFXMgr,然后由SFXMgrAdapter提供各种各样的接口,最后完成转换后调用到SFXMgr::PlaySceneSFX接口完成真正的功能。

适配器模式就是通过包裹一个原有的类,提供客户期待的接口,这样不需要改动到原来的类,也不用客户完成调用的转换细节。适配器有两种实现方式:类适配器和对象适配器。其实就是我们所说的“包裹”是以何种方法实现的。类适配器通过继承来包裹原有的类,提供新接口。对象适配器则通过组合,拥有原有类的一个实例。两种适配器都是为了改变接口,并且最终功能都是在原有类中完成,属于改变接口但不改变行为。

还有一种改变接口但不改变行为的模式——外观(Facade)模式。外观模式同样改变接口,准确的说是简化接口,但底层还是调用原有类的功能。就像上一篇文章提到的,创建场景可能分很多步骤,涉及好几个对象。外观模式就是把原有的接口简化,封装子系统中多个复杂类的调用。

看完两种改变接口但不改变行为的模式,接着我们来看看不改变接口但改变行为的模式。首先是代理(Proxy)模式,代理模式作为结构封装的一员,他的作用是封装一个接口一样但行为不一样的类去代替原来的类完成功能。举个例子,我们有一个PlayerMgr,提供了Add、Remove、Get等操作,现在这个管理器移到另外一个线程了,或者直接移到网络的另外一端了,那么原有的跟PlayerMgr交互的类都得修改。这个时候我们就可以实现一个PlayerMgrProxy,提供PlayerMgr一样的接口,内部实现夸线程或者夸网络的调用,最终通知到PlayerMgr。

代理模式的精髓就是,实现一套原有的接口,替代原有的对象。使用者在使用时,代理就会把调用拦截下来,从而完成一些转发最终调用到原有的对象,或者是自行实现一些其他行为。

另外一个模式,装饰者(Decorator)模式也是保持原有的接口不变,但它不是改变行为,而是添加行为。先来看看类图:

Decorator

假设我们游戏中的装备有增加玩家攻击、防御、敏捷属性,然后装备上可以镶嵌宝石,宝石也会影响装备上面的这三个属性。这种情况下我们尝试用装饰者模式来组织。装饰者模式中基础对象与装饰者都实现同样一套接口,基础对象提供基本功能,例如Armor的GetDefense()返回基础防御100。而装饰者则是在原有对象上再添加功能,例如Ruby的GetDefense()返回当前对象的防御再加10: pObj->GetDefense() + 10。当前对象可以是基础对象,也可以是装饰者,这就是为什么他们要保持同一套接口的原因。

这样我们就可以动态的把1个基础对象与n个装饰者组合成一个对象。例如一个打了2个红宝石,1个蓝宝石的铠甲,它就能非常容易的用装饰者模式表达出来。

IComponent* pObject = new Armor();
pObject = new Ruby(pObject);
pObject = new Ruby(pObject);
pObject = new Sapphire(pObject);

最后pObject 虽然指向一个Sapphire,但它里面包裹着一个Ruby,这个Ruby又包裹着一个Ruby,里面的Ruby包裹着Armor。调用GetDefense()时,则从Sapphire开始,逐层进入到最底的Armor,返回100,再动态的加上各个宝石的加成。

装饰者模式通过层层包裹的方式把对象组合起来,达到动态将功能添加到对象上的目的。这个做法很巧妙,对象之间的组合十分自由,但正因为它的自由性,导致装饰者模式无法控制。首先我们在外层只看到一个统一的接口,对于里面已经包裹了多少东西,包裹了什么东西并不可见。其次我们想在包裹的过程中控制也十分困难,例如我们只允许一件装备最多打3个红宝石这样的需求,在装饰者模式中很难控制。

还有一种设计模式,结构跟装饰者模式非常相似,在UI界面上广发应用,它就是组合(Composite)模式

Composite

之前的文章《浅谈游戏UI架构》里说过,游戏UI中主要分为元件与容器。利用组合模式,我们把原件与容器封装成同一套接口,容器中保存这容器或元件的对象,对于OnKeyDown和OnLButtonClick的这些操作容器直接遍历保存的对象,调用它们的相应方法,最后由子节点的Element来处理。

组合模式的思想就是元件(子节点)与容器(组合)实现同一套接口,在使用时不需要关心使用的是什么。这里会产生一个疑问,一些对于容器才有的操作,例如Add和Remove,元件应该如何处理。通常对于不属于自己的操作,会有一个空实现,调用元件的Add或Remove则什么都不干。可是这样做也会带来一些问题,毕竟往元件上Add东西不是一个合法的操作。另外一个解决办法是提供一个GetType()方法,返回当前对象的类型,通过判断确定是容器再添加。这种做法符合实际操作,消除了不确定性。但使用时则不是完全“一视同仁”了,有点破坏了组合模式的透明性,不过实际运用上大多都会采用这样的实现。

上面我们看到的模式,一种是功能没变,接口改变,一种是接口不变,功能不同。如果我们需求是接口会变,实现也会变,要将两者彻底解耦,这时候桥梁(Bridge)模式就派上用场了。

Bridge

桥梁模式的右边就是一个多态实现,派生类通过派生基类的虚接口实现功能的变化。而左边就是一个类适配器,派生类实现不同的接口,最终调用基类方法,实现接口的变化。两个层次之间通过组合关系实现“桥梁”,达到接口功能都可变的彻底解耦。

不过桥梁模式在游戏工程中的运用并不多,首先是因为功能界面都需要解耦的需求不多,其次是桥梁模式使得结构变得复杂 。在遇到界面或功能要改的时候,可能整理一下现有代码会比直接再架一层更实在。

最后一个结构封装的模式是蝇量(FlyWeight)模式

FlyWeight

蝇量模式的思想是把所有对象的数据都放在一个集合里面,然后提供一个公用的,没有属性的对象代替每一个独立对象,通过集合操作数据。当一个类有许多实例,而这些实例都可以被同一方法控制时,利用蝇量模式可以避免大量对象的存在,从而节省内存。不过写C++的同学会发现,把Player的属性移到管理器里面用数组存储,跟直接放在Player里生成多个Player对象相比,并没有节省内存。的确是,在java或其他带GC的语言中对象是有额外开销的,但在C++里没有这个问题,所以这个模式其实只对带GC的语言有效。

这次介绍了7个从结构上封装的设计模式。包括适配器模式、外观模式这两个改变接口但不改变行为的模式,还有接口不变但功能改变的代理模式、装饰者模式和UI中广泛使用的组合模式。最后还有比较少用的桥梁模式与只适用于带GC语言的蝇量模式。为了讲解套用了一些可能不太现实的例子,不必较真。介绍时所说的实现也并非是标准答案,真正运用起来应该是灵活多变的。抛砖引玉,举得例子都是重在体会思想,开拓思路。^_^

这几篇文章都好长呀,慢慢写吧,下次再来谈谈剩下的模式。

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

Comments are closed.