条款35:考虑virtual函数以外的其他选择
Consider alternatives to virtual functions
现在你正在写一个视频游戏,在游戏中为角色设计了一个继承体系。你决定提供一个成员函数healthValue,它返回一个表示角色健康状况如何的整数。因为不同的角色计算健康值的方法可能不同,将healthValue声明为 virtual似乎是显而易见的设计选择:
class GameCharacter {
public:
virtual int healthValue() const; // 返回人物的健康指数,派生类可重定义它
...
};
healthValue 没有被声明为 pure virtual,暗示这里有一个计算健康值的缺省算法。这确实是一个显而易见的设计选择。但因为这样的设计过于显而易见,可能不会对其它可选方法给予足够的关注。我们来考虑一些处理这个问题的其它方法。
l 借由Non-Virtual Interface手法实现Template Method模式
我们以一个主张virtual函数应该总是为private的有趣观点开始。这一观点的拥护者提出:一个较好的设计应该保留作为public成员函数的healthValue,但应将它改为non-virtual并让它调用一个private virtual函数来做真正的工作(如doHealthValue):
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设计模式(与C++ templates无关)的一个独特表现形式。我将那个non-virtual函数(如healthValue)称为 virtual函数的外覆器。
NVI手法的一个优势通过 "做事前工作" 和 "做事后工作" 两个注释在代码中标示出来。这意味着那个外覆器可以确保在virtual函数被调用前,特定的背景环境被设置,而在调用结束后,这些背景环境被清理。例如,事前工作可以包括锁闭一个mutex,生成一条日志,校验类变量和函数的先决条件是否被满足,等等。事后工作可以包括解锁一个mutex,校验函数的事后条件,再次验证类约束条件,等等。如果你让客户直接调用virtual函),确实没有好的方法能够做到这些。
l 借由Function Pointers实现Strategy模式
NVI手法是public virtual函数有趣的替代方案,但我们还是在用virtual函数来计算每一个角色的健康值。一个更引人注目的设计主张认为计算角色的健康值不依赖于角色的类型,这样的计算根本不需要“角色”。例如,我们可能需要为每一个角色的构造函数传递一个指向健康值计算函数的指针,而我们可以调用这个函数进行实际的计算:
classGameCharacter; // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(constGameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf) {}
int healthValue() const { return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};
这个方法是常见的Strategy设计模式的简单应用,有以下两个优点:
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunchcf = defaultHealthCalc)
: GameCharacter(hcf) { ... }
...
};
int loseHealthQuickly(const GameCharacter&); // 不同的健康指数计算函数
int loseHealthSlowly(const GameCharacter&);
EvilBadGuyebg1(loseHealthQuickly); // 相同类型的人物搭配不同的健康计算方式
EvilBadGuy ebg2(loseHealthSlowly);
换句话说,健康值计算函数不再是GameCharacter继承体系的一个成员函数,意味着它不再拥有访问它所计算的那个对象内部成分的特权。例如,defaultHealthCalc不能访问 EvilBadGuy的non-public成分。实际上,在任何一个你要用类外部的等价机能(如经由一个 non-member non-friend函数或另一个类的非友元成员函数)代替类内部的机能(如经由一个成员函数)的时候,它都是一个潜在的问题。
一般而言,解决“非成员函数对类的非公有成分的访问需要”的唯一方法就是削弱类的封装性。例如,友元或者为其实现的某一部分提供public访问。使用函数指针代替virtual函数的优势(如具有逐对象健康值计算函数的能力和在运行时改变计算函数的能力)是否能封装性的降低,是你必须在设计时就做出决定的重要部分。
l 借由tr1::function完成Strategy模式
如果我们用一个tr1::function类型的对象代替一个函数指针(如前例的healthFunc),我们将有更大的发挥空间。tr1::function类型的对象可以持有任何可调用物(callableentity),如函数指针、函数对象或成员函数指针,只要其签名式兼容于需求端。
此后部分内容没咋看懂,翻到篇帖子讲boost::function的,讲得比较清晰,摘录部分。
原帖地址http://blog.csdn.net/Solstice/article/details/3066268
boost::function就像C#里的delegate,可以指向任何函数,包括成员函数。当用bind把某个成员函数绑到某个对象上时,我们得到了一个closure(闭包)。例如:
class Foo
{
public:
void methodA();
void methodInt(int a);
};
class Bar
{
public:
void methodB();
};
boost::function<void()> f1; // 无参数,无返回值
Foo foo;
f1 = boost::bind(&Foo::methodA, &foo);
f1(); // 调用 foo.methodA();
Bar bar;
f1 = boost::bind(&Bar::methodB, &bar);
f1(); // 调用 bar.methodB();
f1 = boost::bind(&Foo::methodInt, &foo, 42);
f1(); // 调用 foo.methodInt(42);
boost::function<void(int)> f2; // int 参数,无返回值
f2 = boost::bind(&Foo::methodInt, &foo, _1);
f2(53); // 调用 foo.methodInt(53);
如果没有boost::bind,那么boost::function就什么都不是,而有了bind(),“同一个类的不同对象可以delegate给不同的实现,从而实现不同的行为”(myan语),简直就无敌了。
l 古典的Strategy模式
传统的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果看起来就像这样:
当把EvilBadGuy和EyeCandyCharacter作为派生类时,GameCharacter是这个继承体系的根;HealthCalcFunc是另一个带有派生类SlowHealthLoser和FastHealthLoser 继承体系的根;而每一个GameCharacter类型的对象包含一个指向“来自HealthCalcFunc 继承体系对象”的指针。
这就是相应的框架代码:
classGameCharacter; // 前置声明
class HealthCalcFunc {
public:
...
virtual int calc(constGameCharacter& gc) const { ... }
...
};
HealthCalcFunc defaultHealthCalc;
classGameCharacter {
public:
explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
: pHealthCalc(phcf) {}
int healthValue() const { return pHealthCalc->calc(*this);}
...
private:
HealthCalcFunc *pHealthCalc;
};
这个方法的吸引力在于熟悉标准的Strategy模式实现的人可以很快地识别出来,再加上它只要为HealthCalcFunc继承体系添加一个派生类就能提供“将既有健康算法纳入使用”的可能性。
Summary
当你为尝试解决的问题寻求一个设计时,你应该考虑可选的virtual函数替代方案。以下是几个可选方案的快速回顾:
· 使用non-virtualinterface (NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包装较低访问性的virtual函数。
· 用 functionpointer data members(函数指针数据成员)代替 virtual函数,这是 Strategy设计模式的一种表现形式。
· 用 tr1::function成员变量代替 virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的一种形式。
· 用另一个继承体系中的virtual函数代替单独一个继承体系中的virtual函数。这是 Strategy设计模式的传统实现手法。