标签:之间 音乐 rpo 依赖 因此 sys 拆分 范围 设计模式
“使用基类提供的操作集合来定义子类中的行为。“
在游戏中,我们可以实现各种各样的想法,比如说创造一个超级英雄, 我们为超级英雄创造各种能力。这个时候我们可以怎么做了?建立一个superpower的基类,然后使用派生的想法,构建各种派生类来实现超能力。但这里会很快的出现问题,因为超能力的多种多样,我们可能需要在派生类中做各种可能的事情:比如播放音效、产生视觉效果、与AI交互、创建和销毁其它游戏实体以及产生物理效果。它们可能触及代码库的每一个角落。很明显,这样会:
这个时候我们应该怎么办了?对于播放音效,我们可以提供一个playSound的方法,这个方法置于superpower类中,派生的子类需要播放音效的时候都调用这个方法,这样我们就能对音效的播放进行一个统一的管理,比如说调整优先级。而这就引申出了一个做法:把子类需要的功能封装成方法放到基类中,然后让子类访问这些方法。这个时候就有一个问题了,如何安放这些方法了?也就是说子类应该如何来组织使用这些方法实现功能了?为此我们定义一个沙盒方法,这个时子类必须实现的抽象保护方法。所以接下来你要做的就是:
也就是我们把基础的操作代码提取到更高的层次来解决冗余的问题。一旦我们在子类中发现大量的重复代码,我们就会把它上移到基类中作为一个新的基本方法。也就是说我们把子类的耦合都提取到父类中,这样耦合的地方就只有一处,每个子类仅与基类耦合。一旦游戏的某个部分发生变化时,我们只需要修改基类即可,不会牵扯到子类的修改。这样的设计会催生一种扁平的类层次架构。你的继承链不会太深,但会有大量的子类,这些子类与基类挂钩。通过一个类派生大量的子类,我们限制了该代码在代码库中的影响范围。
一个基类定义一个抽象的沙盒方法和一些预定义的操作集合。通过将它们设置为受保护的状态以确保它们仅供子类使用。每个派生出的沙盒子类根据父类提供的操作来实现沙盒函数。
沙盒模式是运用在多数代码库里、甚至游戏之外的一种非常简单通用的模式。如果你正在部署一个非虚的受保护方法,那么你很可能正在使用与之类似的模式。沙盒模式适用于一下情况:
这些年“继承”一词被部分程序圈所诟病,原因之一使基类会衍生越来越多的代码。这个模式尤其受这个因素影响。
由于子类使通过它们的基类来完成各自功能的,因此基类最终会与那些需要与其子类交互的任何系统产生耦合。当然,这些子类也与它们的基类密切相关。这个蜘蛛网式的耦合使得无损的改变基类使很困难的——你遇到类脆弱的基类问题。但从另一个角度来说,你的所有耦合都被聚集到了基类,子类现在与其它部分划清了界限。理想的情况下,你的绝大部分操作都在子类中。这样意味着你的大量代码库使独立的,并且更容易维护。
如果你仍然发现本模式正在把你的基类变得庞大不堪,那么请考虑一些提供的操作提取到一个基类能管理的独立类中。这里可以借鉴组件模式。
示例
superpower基类:
class Superpower { public: virtual ~Superpower(){} protected: virtual void activate() = 0; void move(double x, double y, double z) { //move code } void playSound(SoundId sound) { //play code } //other methods.... };
这里activate就是沙盒函数。由于它是抽象虚函数,因此子类必须要重写它。这是为了让子类实现者能够明确它们该对子类做什么。接下来让我们实现一些子类来说明子类是如何创建的。
class SkyLaunch:public Superpower { protected: virtual void activate() { move(0,0,30); playSound(SOUND_SPROING); } };
这里,子类做的事很简单,移动,然后播放音乐。因为操作都放到了基类中,子类没有与外部代码有任何的耦合。当然,我们可以做其它更复杂的事,只需要在基类中提供相应的基本操作,你可以放飞你的想象力。
沙盒模式就是如此的简单,代码并不太多,它描述的是一个基本的思想,但并没有给出过于详细的机制。所以这里你还是要面临一些抉择:
这里有两个极端,一种是基类什么操作都不提供,只提供一个沙盒方法;而另一个就是基类提供子类所有需要的操作。子类仅与基类耦合,不同调用任何外部系统。前者基类不提供任何操作,所以基类与外部系统的耦合度低,随着基类提供的操作多,与外部系统的耦合就越来越高。如果我们把所有的操作都聚集到基类,那么基类就会变得很大,维护起来也就会越来越困难,所以我们应该如何做出选择了?
这个设计模式的挑战在于最终你的基类可能塞满了方法。你能够通过转移一些函数到其它类中来缓解这种情况,并于基类的相关操作中返回相应的类对象即可。就像这样:
class SoundPlayer { Protected: SoundPlayer& getSoundPlayer() { return soundPlayer_; } private: SoundPlayer soundPlayer_; };
把提供的操作分流到一个像这样的辅助类中能给你带来些好处。
你的基类通常希望封装一些数据以对子类保持隐藏。比如,我们想在系统中添加一些例子特效,那我们如何把粒子系统对象传递给基类了?
像这样:
class Superpower { public: Superpower(ParticleSystem* particles):particles_(particles) {} private: ParticleSystem* particles_; };
这样虽然解决了问题,但同时带来了另一个问题。就是每个继承类都需要一个构造函数来调用基类的构造函数并传递那个粒子系统参数。这样就向每个子类暴露了一些我们并不希望暴露的状态。而且,这样也存在维护负担。如果后面我们添加另一个状态,那么我们不得不修改每个继承类的构造函数来传递它。
为了避免通过构造函数传递所有的东西,我们可以把初始化拆分为两个步骤。构造函数不带参数仅仅负责创造对象,然后我们通过一个直接定义在基类中的函数来传递它所需要的其它数据。比如像这样:
Superpower* power = new SkyLaunch(); power->init(particles);
这里可能发生的就是我们忘记调用init函数,那样我们就只能得到构造了一半的对象。对于这个问题可以通过封装一个方法来解决。
Superpower* createSkyLaunch(ParticleSystem *particles) { Superpower* power = new SkyLaunch(); powwer->init(particles); return power; }
如果你想控制只能使用这个函数来创建SkyLaunch对象,可把SkyLaunch的构造函数声明为私有的来实现,类似单例模式的实现方式。
我们可以把我们不想暴露的状态声明为基类的私有成员,同时也是静态的,游戏将不得不保证初始化这个状态,但它仅需要整个游戏初始化一次,只要保证尽早调用初始化函数即可。这中做法还带来一个好处就是因为是静态变量,所有实例共用一个,所以占用的内存更少。
前面的方法严格要求外部代码必须在基类使用相关状态之前将这些状态传递给基类,这给周围的代码的初始化工作带来了负担。另外一个选择是让基类把它们需要的状态拉进去处理。一个实现方法是使用服务定位器。
class Superpower { protected: void spawnParticles(ParticleType type,int count) { ParticleSystem& particles = Locator::getParticles(); particles.spawn(type,count); } };
这里spawenParticles需要一个粒子系统。它从服务定位器获取了一个,而不是由外部代码主动提供。
标签:之间 音乐 rpo 依赖 因此 sys 拆分 范围 设计模式
原文地址:https://www.cnblogs.com/xin-lover/p/11576081.html