【C++】考虑virtual函数以外的其他选择
假设你正在写一个视频游戏软件,游戏里有各种各样的人物,每个人物都有健康状态,而且不同的人物可能以不同的方式计算他们的健康指数.该如何设计游戏里的人物,主要如何提供一个返回人物健康指数的接口.
方法一,基于虚函数的方法.
在人物角色的基类增加一个成员函数heathValue,返回一个整数,表示人物的健康程度,并将声明为virtual.
1 class GameCharacter { 2 public: 3 virtual int healthValue() const; 4 ... 5 };
heathValue声明为虚函数,因而派生类可以重新定义它,从而获得达到不同的人物可能不同的方式计算他们的健康指数的要求.
但是没有声明为纯函数,这表示会有个计算健康指数的缺省算法.
方法二,藉由Non-Virtual Interface手法实现Template Method模式 NVI
该设计是令客户通过public non-virtual成员函数间接调用private virtual函数,相当对virtual函数进行一层的包装,可以称为是virtual函数的外覆器(warpper).
class GameCharacter { public: int healthValue() const { ... int retVal = doHealthValue(); ... return retVal; } ... private: virtual int doHealthValue() const { ... } };
NVI手法的一个优点可以确保在一个virtual函数被调用之前设定好适当的场景,并在调用结束之后清理场景.
"事前工作"可以包括锁定互斥器,制造运转日志记录项,验证class约束条件,验证函数先决条件等等.
"事后工作"可以包括互斥器解除锁定,验证函数的事后条件,再次验证class约束条件等等.
方法三,藉由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; };
该方法的优点,同一人物类型之不同实体可以有不同的健康计算函数,只需要在构造实例时,传入不同的计算函数的指针.
某已知人物之健康计算函数可在运动期变更,可以在GameCharacter里提供一个成员函数setHealthCalc,用来替换当前的健康指数计算函数.
该方法的缺点,如果需要利用GameCharacter的non-public信息进行计算健康指数时,由于计算函数是non-member non-friend函数,将出现无法访问的问题.
如果让计算函数访问成功,则需要降低GameCharacter的封装性.
方法四,藉由tr1::function完成Strategy模式
不再使用函数指针,而是改用一个类型为tr1::function的对象.
可以是函数指针,函数对象,或成员函数指针,只要其签名式兼容于需求端.
class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc; explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf) {} int healthValue() const { return healthFunc(*this); } ... private: HealthCalcFunc healthFunc; }; short calcHealth(const GameCharacter&); struct HealthCalculator { int operator()(const GameCharacter&) const {...} }; class GameLevel { public: float health(const GameCharacter&) const; ... }; class EvilBadGuy:public GameCharacter{ ... }; EvilBadGuy ebg1(calcHealth); //函数 EvilBadGuy ebg2(HealthCalculator()); //函数对象 GameLevel currentLevel; EvilBadGuy ebg3(std::tr1::bind(&GameLevel::health,currentLevel,_1); //成员函数
优点,以tr1::function替换函数指针之后,可以允许客户在计算人物健康指数时使用任何兼容的可调用物.
方法五,古典的Strategy模式
将健康计算函数做成一个分离的继承体系中的virtual成员函数.
每个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象
class GameCharacter; class HealthCalcFunc { public: ... virtual int calc(const GameCharacter& gc) const {...} ... }; HealthCalcFunc defaultHealthCalc; class GameCharacter { public: explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc):pHealthFunc(phcf) {} int healthValue() const { return pHealthFunc->calc(*this); } ... private: HealthCalcFunc* pHealthFunc; };
优点,只要为HealthCalcFunc继承体系添加一个派生类,就可以将一个既有的健康算法纳入使用.
启发:针对具体的应用问题,需要认真分析其应用的特点,以及应用的后续扩展等问题,再从众多的方法,选取最合适的方法.
不能先入为主的,随便的套用一个方法,这样可能会导致应用的后续扩展问题.总之,遇到问题,先想,再比较,最后确定方案.
参考资料:Effective C++中文版