假设你正在写一个视频游戏软件,你打算为游戏内的人物设计一个继承体系。你的游戏术语暴力砍杀类型,剧中人物被伤害或因其他因素而降低健康状态的情况并不罕见。你因此决定提供一个成员函数healthValue,它会返回一个整数,表示人物的健康程度。由于不同的人物可能以不同的方式计算他们的健康指数,将healthValue声明为virtual似乎是再明白不过的做法:
class GameCharacter {
public:
virtual int healthValue() const; // 返回人物的健康指数
... // derived classes可重新定义它
};
healthValue并未被声明为pure virtual,这暗示我们将会有个计算健康指数的缺省算法(见条款34)。
这的确是再明白不过的设计,但是从某个角度说却反而成了它的弱点。由于这个设计如此明显,你可能因此没有认真考虑其他代替方案。为了帮助你跳脱面向对象设计路上的常轨,让我们考虑其他一些解法。
籍由Non-Virtual Interface手法实现Template Method模式
我们将从一个有趣的思想流派开始,这个流派主张virtual函数应该几乎总是private。这个流派的拥护者建议,较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:
class GameCharacter {
public:
int healthValue() const // derived classes不重新定义它
{ // 见条款36
... // 做一些事前工作,详下
int retVal = doHealthValue(); // 做真正的工作
... // 做一些事后工作,详下
return retVal;
}
...
private:
virtual int doHealthValue() const // deri classes可重新定义它
{
... // 缺省算法,计算健康指数
}
};
在这段(以及本条款其余的)代码中,我直接在class定义式内呈现成员函数本体,一如条款30所言,那也就让它们全都暗自成了inline,但其实我以这种方式呈现代码只是为了让你比较容易阅读。我所描述的设计与inlining其实没有关联,所以请不要以为成员函数在这里被定义于classes内有特殊用意。不,它没有。
这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates并无关联)的一个独特表现形式。我把这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。
籍由Function Pointers实现Strategy模式
NVI手法对public virtual函数而言是一个有趣的替代方案,但从某种设计角度观之,它只比窗饰花样更强一些而已。毕竟我们还是使用virtual函数来计算每个人物的健康指数。另一个更戏剧性的设计主张“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分,例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
class GameCharacter; // 前置声明(forward declaration)
// 以下函数是计算健康指数的缺省算法。
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&); // 健康指数计算函数1
int loseHealthSlowly(const GameCharacter&); // 健康指数计算函数2
EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的人物搭配
EvilBadGuy ebg2(loseHealthSlowly); // 不同的健康计算方式
- 某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
换句话说,“健康指数计算函数不再是GameCharacter继承体系内的成员函数”这一事实意味,这些计算函数并未特别访问“即将被计算健康指数”的那个对象的内部成分。例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。
如果人物的健康可纯粹根据该人物public接口得来的信息加以计算,这就没有问题,但如果需要non-public信息进行精确计算,就有问题了。实际上任何时候当你将class内的某个机能(也许取道自某个成员函数)替换为class外部的某个等价机能(也许取道自某个non-member non-friend函数或另一个class的non-friend成员函数),这都是潜在争议点。
一般而言,唯一能够解决“需要以non-member函数访问class的non-public成分”的办法就是:弱化class的封装。例如class可声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数(其它部分则宁可隐藏起来)。运用函数指针替换virtual函数,其优点(像是“每个对象可各自拥有自己的健康计算函数”和“可在运行期改变计算函数”)是否足以弥补缺点(例如可能必须降低GameCharacter封装性),是你必须根据每个设计情况的不同而抉择的。
籍由tr1::function完成Strategy模式
一旦习惯了templates以及它们对隐式接口(见条款41)的使用,基于函数指针的做法看起来便过分苛刻而死板了。为什么要求“健康指数之计算”必须是个函数,而不能是某种“像函数的东西”(例如函数对象)呢?如果一定得是函数,为什么不能够是个成员函数?为什么一定得返回int而不是任何可被转换为int的类型呢?
如果我们不再使用函数指针(如前列的healthFunc),而是改用一个类型为tr1::function的对象,这些约束就全都挥发不见了。就像条款54所说,这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象或成员函数指针),只要其签名式兼容于需求端。以下将刚才的设计改为使用tr1::function:
class GameCharacter; // 如前
int defaultHealthCalc(const GameCharacter& gc); // 如前
class GameCharacter {
public:
// HealthCalcFunc可以是任何“可调用物”(callable entity),可被调用并接受
// 任何兼容于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;
};
如你所见,HealthCalcFunc是个typedef,用来表现tr1::function的某个具现体,意味该具现体的行为像一般的函数指针。现在我们靠近一点瞧瞧HealthCalcFunc是个什么样的typedef:
std::tr1::function<int (const GameCharacter&)>
那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”。这个tr1::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物(callable entity)。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。
和前一个设计(其GameCharacter持有的是函数指针)比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。
古典的Strategy模式
如果你对设计模式(design patterns)比对C++的酷劲更有兴趣,我告诉你,传统(典型)的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果看起来像这样:
如果你并未精通UML符号,别担心,这图只是告诉你GameCharacter是个继承体系的根类,体系中EvilBadGuy和EyeCandyCharacter都是derived classes:HealthCalcFunc是另一个继承体系的根类,体系中的SlowHealthLoser和FastHealthLoser 都是derived classes,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。
下面是对应的代码骨干:
class GameCharacter; // 前置声明(forward declaration)
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;
};
这个解法的吸引力在于,熟悉标准Strategy模式的人很容易辨认它,而且它还提供“将一个既有的健康算法纳入使用”的可能性——只要为HealthCalcFunc继承体系添加一个derived class即可。
摘要
本条款的根本忠告是,当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案。下面快速重点覆写我们验证过的几个替代方案:
- 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。
- 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
- 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
- 将继承体系内的virtual函数替换为另一个继承体系的virtual函数。这是Strategy设计模式的传统实现手法。
以上并未彻底而详尽地列出virtual函数的所有替换方案,但应该足够让你知道的确有不少替换方案。此外,它们各有其相对的优点和缺点,你应该把它们全部列入考虑。
请记住
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无访问class的non-public成员。
- tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。