条款35:考虑虚函数以外的其它选择
1.1 提出问题
假设正在制作一款游戏,正在为游戏中的角色设计一个层次结构。
cpp
class GameCharacter {
public:
virtual int healthValue() const; //返回角色的生命值;派生类可以重新定义它
... //
};
1.2 解决办法
让我们考虑一些其他的方法来实现同样的效果:
1.2.1 非虚接口
藉由非虚接口(non-virtual interface,NVI)实现 模板方法(Template Method)设计模式:
cpp
class GameCharacter {
public:
//healthValue负责包裹doHealthValue
int healthValue() const // NVI,不允许派生类重新定义
{
... // 做一些"事前准备"工作(框架的一部分)
int retVal = doHealthValue(); // 做真正的工作
... // 做一些"事后清理"工作(框架的一部分)
return retVal;
}
...
private:
virtual int doHealthValue() const // 被隐藏在private里的虚函数,运行重写
{
... // 计算角色生命值的默认算法
}
}
如果让客户重写healthValue,则无法确保框架部分的执行。
1.2.2 函数指针
藉由函数指针(Function Pointer)实现策略(Strategy)设计模式:
cpp
class GameCharacter; // 前置声明
int defaultHealthCalc(const GameCharacter& gc);// 计算健康状况的默认算法
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;// 函数指针
};
相同角色类型的不同实例可以具有不同的生命值计算功能。
cpp
class EvilBadGuy : public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)//初始化基类部分
{
...
}
...
};
//还可以通过添加成员函数,在运行时改变健康值的计算行为
int loseHealthQuickly(const GameCharacter&); // 健康值计算函数1
int loseHealthSlowly(const GameCharacter&); // 健康值计算函数2
EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的实例
EvilBadGuy ebg2(loseHealthSlowly); // 具有不同的健康值计算行为
注:非成员函数,如果需要访问private成员,需要设置友元或添加接口,会破坏类的封装性。
1.2.3 std::function
藉由 std::function 实现策略(Strategy)设计模式:
cpp
class GameCharacter; // 和以前一样
int defaultHealthCalc(const GameCharacter& gc); // 和以前一样
class GameCharacter {
public:
// HealthCalcFunc是"可调用的实体(callable entity)"
// 它可以接受任何与GameCharacter兼容的东西,并返回任何与int兼容的东西
typedef std::function<int(const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
小小的一个变化,但结果是客户现在在指定生命值计算函数方面具有了惊人的灵活性:
cpp
short calcHealth(const GameCharacter&); // 健康值计算函数,返回类型不是int
struct HealthCalculator { // 函数对象:计算健康值
int operator()(const GameCharacter&) const
{
...
}
};
class GameLevel {
public:
float health(const GameCharacter&) const; // 成员函数:计算健康值
...
};
class EvilBadGuy : public GameCharacter { // 和以前一样
...
};
class EyeCandyCharacter : public GameCharacter { // 另一种人物类型
...
};
EvilBadGuy ebg1(calcHealth);
EyeCandyCharacter ecc1(HealthCalculator());
GameLevel currentLevel;
...
EvilBadGuy ebg2(
std::bind( &GameLevel::health, currentLevel, _1)
);
1.2.4 古典的策略(Strategy)设计模式:
使健康值计算函数成为独立的层次结构的虚成员函数,这种方法提供了一种可能性,可以通过向HealthCalcFunc层次结构添加派生类来调整现有的健康值计算算法。
对应的代码框架:
cpp
class GameCharacter; // 前置声明
class HealthCalcFunc {
public:
...
virtual int calc(const GameCharacter& gc) const
{
...
}
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthCalc(phcf)
{}
int healthValue() const
{
return pHealthCalc->calc(*this);
}
...
private:
HealthCalcFunc* pHealthCalc;
};
1.3 总结
- virtual函数的替代方案包括NVI和策略设计模式的多种形式。NVI本身是一种特殊形式的Template Method设计模式。
- 将功能从成员函数移到class外部,带来一个缺点,非成员函数无法访问class的非public成员。
- function对象的行为就像一般函数指针。这样的对象可接纳"满足目标签名式"的所有可调用实体。