理论要点
什么是子类沙箱:用一系列由基类提供的操作定义子类中的行为。通俗地讲就是把公共的操作都封装进基类,子类不直接与外部其它模块耦合,把耦合集中到基类统一处理。
要点:基类定义抽象的沙箱方法和几个提供操作的实现方法,将他们设为protected,表明它们只为子类所使用。每个推导出的沙箱子类用提供的操作实现了沙箱方法。
使用场合:
子类沙箱模式是潜伏在编程日常中简单常用的模式,哪怕是在游戏之外的地方。 如果有一个非虚的protected方法,你可能早已在用类似的技术了。
沙箱方法在以下情况适用:- 你有一个能推导很多子类的基类。
- 基类可以提供子类需要的所有操作。
- 在子类中有行为重复,你想要更容易的在它们间分享代码。
- 你想要最小化子类和程序的其他部分的耦合。
代码分析
1,假设我们要实现一个超级英雄它在游戏中有成百上千种不同的超级能力可供选择。如天上飞的,地上跑的,水里游的,等等
像这种同类型很多子类的情形,我们就可以自然而然的使用子类沙箱呢!
我们从Superpower基类开始:
class Superpower
{
public:
virtual ~Superpower() {}
protected:
virtual void activate() = 0;
void move(double x, double y, double z)
{
// 实现代码……
}
void playSound(SoundId sound, double volume)
{
// 实现代码……
}
void spawnParticles(ParticleType type, int count)
{
// 实现代码……
}
};
activate()方法是沙箱方法。由于它是抽象虚函数,子类必须重载它。 这让那些需要创建子类的人知道要做哪些工作。
其他的protected函数move(),playSound(),和spawnParticles()都是提供的操作。 它们是子类在实现activate()要调用的。
好了,既然实现了公共的基类,那么我们就可以随心而欲的添加各种特性的英雄呢!首先来个能跳跃空中的英雄:
class SkyLaunch : public Superpower
{
protected:
virtual void activate()
{
// 空中滑行
playSound(SOUND_SPROING, 1.0f);
spawnParticles(PARTICLE_DUST, 10);
move(0, 0, 20);
}
};
这个很简单,所有操作都在基类中,子类不直接与外部代码耦合。下面我们再来让这个英雄精细些,我们在基类中再新增些接口:
class Superpower
{
protected:
double getHeroX()
{
// 实现代码……
}
double getHeroY()
{
// 实现代码……
}
double getHeroZ()
{
// 实现代码……
}
// 退出之类的……
};
我们增加了些方法获取英雄的位置。我们的SkyLaunch现在可以使用它们变丰富了:
class SkyLaunch : public Superpower
{
protected:
virtual void activate()
{
if (getHeroZ() == 0)
{
// 在地面上,冲向空中
playSound(SOUND_SPROING, 1.0f);
spawnParticles(PARTICLE_DUST, 10);
move(0, 0, 20);
}
else if (getHeroZ() < 10.0f)
{
// 接近地面,再跳一次
playSound(SOUND_SWOOP, 1.0f);
move(0, 0, getHeroZ() + 20);
}
else
{
// 正在空中,跳劈攻击
playSound(SOUND_DIVE, 0.7f);
spawnParticles(PARTICLE_SPARKLES, 1);
move(0, 0, -getHeroZ());
}
}
};
是不是天高任鸟飞!如果要实现其它什么特性的英雄也很方便扩展。
2,子类沙箱到现在其实已经进完了,真没什么东西,应该是到目前为止最简单的策略了,而且都是那种需要时自然而然会使用上的模式。
下面我们仅谈谈一些注意策略:
首先,问题一:这个模式是把所有操作往基类中塞,最终肯定会让基类充斥着很多的方法,举个例子,为了让英雄播放声音,我们可以直接将它们加到Superpower中:
class Superpower
{
protected:
void playSound(SoundId sound, double volume)
{
// 实现代码……
}
void stopSound(SoundId sound)
{
// 实现代码……
}
void setVolume(SoundId sound)
{
// 实现代码……
}
// 沙盒方法和其他操作……
};
如果Superpower已经很庞杂了,我们也许想要避免这样。 取而代之的是创建SoundPlayer类来二次封装这些方法:
class SoundPlayer
{
void playSound(SoundId sound, double volume)
{
// 实现代码……
}
void stopSound(SoundId sound)
{
// 实现代码……
}
void setVolume(SoundId sound)
{
// 实现代码……
}
};
然后这样来减少基类中庞大的方法:
class Superpower
{
protected:
SoundPlayer& getSoundPlayer()
{
return soundPlayer_;
}
// 沙箱方法和其他操作……
private:
SoundPlayer soundPlayer_;
};
接下来,看看问题二:如果基类需要外部对象来构造,那么这应该怎样传递这个对象参数呢?如Superpower类提过了spawnParticles()方法。 如果方法的实现需要一些粒子系统对象,怎么获得呢?最简单的也许是这样:
class Superpower
{
public:
Superpower(ParticleSystem* particles)
: particles_(particles)
{}
// 沙箱方法和其他操作……
private:
ParticleSystem* particles_;
};
然后子类需要这样构造:
class SkyLaunch : public Superpower
{
public:
SkyLaunch(ParticleSystem* particles)
: Superpower(particles)
{}
};
我们在这看到了问题。每个子类都需要构造器调用基类构造器并传递变量。这让子类接触了我们不想要它知道的状态。
这也造成了维护的负担。如果我们后续向基类添加了状态,每个子类都需要修改并传递这个状态。
因此,通过构造函数传递外部变量不是我们想要的,那么怎么去掉这个带参的构造形式呢?
方法一:类似我们cocos游戏引擎中create创建节点的做法,使用两阶初始化,如这样:
Superpower* create(ParticleSystem* particles)
{
Superpower* power = new SkyLaunch();
power->init(particles);
return power;
}
方法二:如果这个外部传入的对象可以被所有子类所共享,不需要为每个子类创建单独的状态,那么这其实可以让这个对象静态化:
class Superpower
{
public:
static void init(ParticleSystem* particles)
{
particles_ = particles;
}
// 沙箱方法和其他操作……
private:
static ParticleSystem* particles_;
};
注意这里的init()和particles_都是静态的。 只要游戏早先调用过一次Superpower::init(),那么所有子类都能接触粒子系统。
更棒的是,现在particles_是静态变量, 我们不需要在每个Superpower中存储它,这样我们的类占据的内存更少了。
方法三:上面两种方式都是要求把对象的初始化责任交给周围的代码。我们还有一种策略,让外面都不能管这个对象的初始化,使用服务定位器模式,让基类自己拉取它需要的对象:
class Superpower
{
protected:
void spawnParticles(ParticleType type, int count)
{
ParticleSystem& particles = Locator::getParticles();
particles.spawn(type, count);
}
// 沙箱方法和其他操作……
};
好, 这个模式很简单,不多说了,结束!