条款35:考虑virtual函数以外的其他选择
假设我们正在设计一款游戏,剧中人物被伤害而降低健康状态的情况并不罕见,因此我们要为此写一个helthValue函数,他返回一个代表健康值的整数,由于不同的人物可能以不同的方式计算他们的健康值,所以可以将healthValue声明为virtual函数:
class GameCharacter {
public:
virtual int healthValue() const; // 返回人物的健康指数
... // 派生类可重新定义它
};
healthValue并未被声明为纯虚函数,这暗示我们将会有个计算健康指数的缺省算法。
为了帮助跳脱1面向对象设计路上的常规,让我们考虑其他一些解法。
藉由Non-virtual Interface手法实现Template Method模式
有一种流派思想是主张virtual函数应该几乎总是private。因此本设计应该保留healthValue为public成员函数,但让他成为non-virtual,并调用一个private virtual函数:
class GameCharacter {
public:
int healthValue() const
{
... //做一些事前工作
int retVal = doHealthValue(); //做真正的工作
... //做一些事后工作
return retVal;
}
private:
virtual int doHealthvalue() const
{
... //缺省算法,计算健康值
}
};
这一基本设计,也就是令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。它是所谓的template Method设计模式的一个独特表现形式。
在NVI手法下其实没有必要让virtual函数一定得是private。某些class继承体系要求派生类在virtual函数的实现内必须调用其base class的对应兄弟,而为了这样的调用合法,virtual函数必须是protected,不能是private。有时候virtual函数一定得是public,这样一来就不能实施NVI手法了。
藉由Function Pointers实现strategy模式
另一个设计主张是“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“任务这个成分”。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc) (const GameCharacter&);
explicit GameCharater(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this);}
...
private:
HealthCalcFunc healthFunc;
};
这就是常见的Strategy设计模式的简单应用。它提供了某些有趣弹性:
- 同一人物类型之不同实体可以有不同的健康计算函数。例如:
class EviBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc):
GameCharater(hcf){...}
...
};
int loseHealthQuickly(const GameCharacter&); //健康值计算函数1
int loseHealthSlowly(const GameCharacter&); //函数2
EvilBadGuy ebdg1(loseHealthQuickly);
EvilBadGuy ebd2(loseHealthSlowly);
- 某已知人物之健康指数计算函数可在运行期间变更。例如GameCharacter可提供一个成员函数SetHeathCalculator,用来替换当前的健康值计算函数。
藉由tr1::function完成Strategy模式
tr1::function类型地对象可持有任何可调用物,也就是函数指针、函数对象或成员函数指针,只要其签名式兼容于需求端。以下将是刚才设计改为使用tr1::function:
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharater {
public:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hfc){}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
和前一个设计比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。这个改变具有更强大的弹性:在指定健康计算函数函数时,函数的返回值可以不是int型,而只要是能够隐式转换为int型即可;可指定成员函数来进行计算等。
古典的Strategy模式
下面代码是一种标准的Strategy模式:
class GameCharacter;
class HealthCalcFunc {
public:
...
virtual int calc(const GameCharacter& gc) const
{...}
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc): pHeathCalc(phcf) {}
int healthValue() const
{return pHealthCalc->calc(*this);}
...
private:
HealthCalcFunc* pHealthCalc;
};
请记住
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到class外部函数,带来一个缺点是,非成员函数无法访问class的non-public成员。
- tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
条款36:绝不重新定义继承而来的non-virtual函数
class B {
public:
void mf();
};
class D: public {
public:
void mf();
};
D x;
B* pB = &x;
pB->mf(); //调用B::mf
D* pD = &x;
pD->mf(); //调用D::mf
造成此一两面行为的原因是,non-virtual函数如B::mf和D::mf都是静态绑定。这意思是,由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B派生之class”的对象。
- 适用于B对象的每一件事,也适用于D对象,因为每个D对象都是一个B对象;
- B的派生类一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数。