假设你整在写一个视频游戏软件,由于不同的人物可能以不同的方式计算它们的健康指数,将healthValue声明为virtual似乎再明白不过的做法:
class GameCharacter {
public:
virtual int healthValue()const;
...
};由于这个设计如此明显,你可能没有认真考虑其他替代方案。为了帮助你跳脱面向对象设计路上的常轨,让我们考虑其他一些解法:
藉由Non-virtual interface手法实现Template Method模式
有个思想流派主张virtual函数应该几乎总是private。他们建议,较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:
class GameCharacter {
public:
int healthValue() const
{
...//做一些事前工作
int retVal = doHealthValue();
...//做一些事后工作
return retVal;
}
private:
virtual int doHealthValue() const //derived class 可以重新定义它。
{
...//缺省计算,计算健康指数
}
};这一基本设计,“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。是所谓template Method设计模式的一个独特表现形式。把这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。
NVI的优点在上述代码注释“做一些事前工作”和“做一些事后工作”之中。这意味着外覆器(wrapper)确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。“事前工作”可以包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等。
NVI手法涉及在derived class内重新定义private virtual函数。重新定义若干个derived class并不调用的函数!这里并不矛盾。“重新定义virtual函数”表示某些事“如何”被完成,“调用virtual函数”则表示它“何时”被完成。NVI允许derived class重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权力。
NVI手法其实没必要让virtual函数一定是private。有时必须是protected。还有时候甚至是public,这么一来就不能实施NVI手法了。
藉由Function Pointers实现Strategy模式
另一个设计主张是“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
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;
};这个做法是常见的Strategy设计模式的简单应用。和“GameCharacter继承体系内的virtual函数”的做法比较,它提供了某些有趣弹性:
同一人物类型不同实体可以有不同的健康计算函数。例如:
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{...}
...
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);EvilBadGuy ebg1(loseHealthSlowly);//相同类型的人物搭配
EvilBadGuy ebg2(loseHealthQuickly);//不同的健康计算方式某已知人物健康指数计算函数可以在运行期变更:例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
这些计算函数并未特别访问“即将被计算健康指数”的那个对象内部成分。例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。如果需要non-public信息进行精确计算,就有问题了。唯一能解决“需要以non-member函数访问class的non-public成分”的办法就是:弱化class的封装。例如class可以声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数。利用函数指针替换virtual函数,其优点(像是“每个对象可各自拥有自己的健康计算函数”和“可在运行期间改变计算函数”)是足以弥补缺点(例如可能必须降低GameCharacter封装性)。
藉由tr1::function完成Strategy模式
我们不再用函数指针,而是用一个类型为tr1::function的对象,这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象、或成员数函数指针),只要其签名式兼容于需求端。
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
//HealthCalcFunc可以是任何“可调用物”,可被调用并接受
//任何兼容于GameCharacter之物,返回任何兼容于int的东西。
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};这里我们把tr::function的具现体的目标签名式以不同颜色强调出来。那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”
std::tr1::function<int (const GameCharacter&)>
所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换成int。
客户在“指定健康计算函数”这件事上有更惊人的弹性:
short calcHealth(const GameCharacter&); //函数return non-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::tr1::bind(&GameLevel::health, currentLevel, _1));//成员函数GameLevel::health宣称它接受两个参数,但实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter和一个GameLevel),转而接受单一参数(GameCharacter)。于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为。
古典的Strategy模式
将健康计算函数做成一个分离的继承体系中的virtual成员函数。
class GameCharacter;
class HealthCalcFunc {
...
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;
};
每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。
还可以提供“将一个既有的健康计算算法纳入使用”的可能性--只要为HealthCalcFunc继承体系添加一个derived class即可。