条款35: 考虑virtual函数以外的选择
内容:
最近比较忙,好几天没更新了,今天拼了命抽出时间来补上,呵呵,废话就不多说了,进入正题。
假如现在你正在写一个游戏软件,游戏里面有各种游戏任务角色,人一多了嘛,就容易出现各个方面的利益冲突,而游戏设计者让他们解决冲突的直接方法就是--战斗,于是游戏中各种人物相互之间砍杀的画面就经常出现,这样就出现了由于受伤或者其它因素导致了健康系数的下降,这个时候作为游戏设计者的你,显然在这里要提供一个函数来计算各种人物当前的健康系数。这个难不倒你,由于各中人物的角色不同,身体素质不同等决定了它们的健康系数也不同,用virtual函数来计算看来是很合理的想法。
class GameCharacter{
public:
virtual int healthValue()const; // 返回人物的健康指数并提供默认的实现体, 子类可以改写
...
};
这时候有个思想流派主张所有virtual函数都应当为private,其子类可以改写该函数,于是他们就提出了他们对这个问题的建议,让non-virtual成为public接口来提供游戏人物的健康指数,而在内部该函数调用private virtual函数(真正负责计算健康指数)。
class GameCharacter{
public:
int healthValue()const{
... //调用之前准备工作
int value = calcHealthValue();
...//调用之后的一些清理工作等
}
...
private:
virtual int calcHealthValue()const{
...
}
...
};
以上这种"令客户通过public non-virtual成员函数间接调用private virtual函数",称为non-virtual interface(NVI) 手法。它是所谓的Template Method涉及模式(与C++ templates并无关联)的一个独特表现形式,而这个public non-virtual(healthValue)称为virtual函数的wrapper。这种手法的好处是很明显:我们可以在"真正做一些工作"之前”做一些准备工作“,而在调用完成之后我们也具有”后期处理事情“的能力。而它的缺点也有,其中一点是该手法的应用范围,比如某些类继承体系要求子类的virtual函数必须调用基类的对应兄弟,为了让这样合法,virtual函数就必须是protected。而有时候甚至一定得是public(例如具备多态性的析构函数),这样依赖NVI就没法用了。
于是我们的NVI替代方案又出来了,我们可以通过function pointer来实现strategy来解决本款刚刚
提出的问题。
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
typedef int (*HealthCalcFun)(const GameCharacter&);
explicit GameCharater(HealthCalcFun hcf = defaultHealthCalc)
:health_calculate_function_(hcf){
}
int healthValue()const{
return health_calculate_function_(*this);
}
...
private:
HealthCalcFunc health_calculate_function_;
};
这种手法比NVI提供了更多的弹性:它既可以为一种游戏角色提供不同的健康指数计算方法,如果你再提供像setHealthFun类似的接口以后,我们就可以在运行期间更改同一角色人物的健康指数计算方法。多好啊!先不要激动,这里好像有一个问题:这里的健康指数计算函数不需要去访问类的non-public成分,这样当然没用问题,但如果我们提供的健康指数函数需要访问GameCharater内部成分即非non-public的话,这就有问题了,难道我们不得不去让该函数成为GameCharater类友元或通过其它方式才能解决问题(这显然降低了该类的封装性)。当你面对这样问题的时候你就要重新考虑你的设计策略的优点是否足以弥补缺点而进行抉择了。
上面这种基于函数指针的做法看起来是不是那么的死板和苛刻,呵呵,为什么一定要是一个函数指针呢?为什么不是一个“像函数的东西”呢?为什么这个函数就不能够是个成员函数呢?为什么函数一定要返回int而不是可被转换为int的类型呢?为了解决这些疑问,我们拿出了我们的重量级武器--tr1::function.先将刚才的设计修改一下:
...
class GameCharacter{
public:
typedef std::tr1::function<int (const GameCharacter& )> HealthCalcFunc;
.... //同上
};
悠呵,这个typedef这么大的一块是个啥玩意啊?
std::tr1::function<int (const GameCharacter& )>
实际上这个签名代表的是"接受一个reference指向const GameCharaceter,并返回int".而我们定义的这个
HealthCalcFunc可以保持任何与此签名式兼容的可调用物。这里的兼容指的是可调用物的参数可被隐式转换为const GameCharaceter&,而其返回类型可被隐式转换为int。下面我们来测试其效果:
short calcHealth(const GameCharaceter&); //健康指数计算函数,注意它的返回类型为non-int
struct HealthCalculator{ //为计算健康而设计的函数对象
int operator(const GameCharaceter&)const{...}
};
class GameLevel{
public:
float health(const GameCharacter&)const; //为计算健康指数设计的成员函数
...
}
//定义两种游戏角色
class EvilBadBuy:public GameCharaceter{
... //同前面
};
class EyeCandyCharacter:public GameCharacter{
... //同前面
};
EvilBadGuy ebg1(calcHealth); //人物1 使用函数计算健康指数
EyeCandyCharacter ecc1(HealthCalculator());//人物2:使用函数对象计算健康指数
GameLevel curLevel;
...
//人物3:使用某个成员函数计算健康指数
EvilBadGuy ebg2( std::tr1::bind( &GameLevel::health, curLevel, _1 ) );
wow!!!这个trl::function也忒强大了吧!最后一个函数tr1::bind貌似生面孔,它是做什么滴呢?我们要知道GameCharacter::health实际上接受两个参数(第一个是隐式参数---该类的对象地址,即this,别忘记!)而HealthCalcFunc却只能接受一个参数,于是我们必须进行转换,所以我们使用curLevel作为该成员函数的第一个参数的值,让ebg2总是以curLevel对象的健康函数来计算,这就是tr1::bind的作用。"_1"意味着"当为ebg2调用GameLevel::health时以curLevel作为GameLevel对象".
如果你对设计模式感兴趣的话,传统的Strategy模式做法会将健康函数做成一个分离的继承体系中的virtual成员函数,设计的结果图本来是要帖出来的,不过该论坛的贴图功能好像不这么完善,二者大多数人喜欢直接看代码,那么我就把骨干代码帖出来:
class GameCharacter;
class HealthCalcFunc{
public:
...
virtual int calc(const GameCharacter& gc)const{
...
}
...
};
HealthCalcFunc default_health_calculator;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* function = &default_health_calculator )
:health_calculator_function(function){}
int healthValue()const{
return health_calculator_function_->calc(*this);
}
...
private:
HealthCalcFunc* health_calculator_function_;
};
接下来我们来快速复习我们验证过的几个替代virtual函数的方案:
◆ 使用non-virtual interface(NVI)手法。
◆ 函数指针作为成员变量。
◆ 以tr1::function成员变量替换virtual函数。
◆ 传统的Strategy设计模式的实现手法。
好,今天敲的够多的了!
请记住:
■ virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的template Method 设计模式。
■ 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的 non-public成员。
■ tr1::function对象的行为就像一般函数指针。这样的对象可接纳"与给定之目标签名式兼容"的所以可调 用物。