- 假设现在有这样的一个程序:
- 正在写一个视频游戏软件,现在为游戏内的任务设计一个继承体系
- 现在我们定义一个函数healthValue(),用来表示人物的健康状态,其返回一个整数
- 由于不同的人可能会以不同的方式计算他们的健康指数,因此我们可能会想到将healthValue()函数定义为virtual。如下所示
class GameCharacter {public://返回人物的健康指数,派生类可以重新定义它virtual int healthValue() const;};
- 上面这个设计方案虽然可行,但是从某个角度来说却成了它的弱点。本文将讨论有没有别的方法来避免使用virtual函数
一、藉由Non-Virtual Interface手法实现Template Method模式
- 此手法主张的做法:
- 将healthValue()函数声明为public,并且改为non-virtual函数
- 再设计一个private virtual函数,将healthValue()原本的功能移至该函数中,然后在healthValue()函数中调用该函数
- 代码如下:
class GameCharacter {public://派生类不应该重新定义它int healthValue()const {//... //事前工作int retVal = doHealthValue();//.. //事后工作return retVal;}private://返回人物的健康指数,派生类可以重新定义它virtual int doHealthValue()const {}};
- 注意事项:成员函数在类中进行定义就会变为inline,但是此处不是,此处只是为了演示代码而已
NVI手法特点
- 令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式的一个独特表现形式。我们把这个non-virtual 函数称为virtual函数的外覆器
- NVI手法的优点:我们可以在non-virtual函数中做一些其他事情。例如:
- 事前工作:可以进行锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等
- 事后工作:可以进行互斥器解锁、验证函数的事后条件、再次验证class约束条件等等
- 这些优点是在客户端直接调用virtual函数的情况中做不到的
- NVI手法可以在派生类中重新定义private virtual函数:
- 重新定义virtual函数:表示某些事“如何”被完成
- 调用virtual函数:表示它合适被完成
- 这两件事情互不干扰。因此NVI手法允许在派生类中重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力
class GameCharacter {//同上private:virtual int doHealthValue()const {std::cout <<"Base" <healthValue(); //打印&#xff1a;BaseGameCharacter *p2 &#61; new Hero;p2->healthValue(); //打印&#xff1a;Derivedreturn 0;}
- 关于virtual函数的访问级别&#xff1a;
- 在NVI手法下其实virtual函数不一定得是private的
- 在有些情况下&#xff0c;要求派生类在virtual函数中调用基类的virtual函数&#xff08;参阅条款27&#xff09;&#xff0c;那么当virtual函数在基类中为private之后&#xff0c;派生类就不可以访问了。因此&#xff0c;在这种情况下&#xff0c;virtual函数可以设置为protected
- 在NVI手法下&#xff0c;virtual函数不要设置为public&#xff0c;因为设置为public之后&#xff0c;就与NVI手法的初衷相反了&#xff0c;失去了封装性
二、藉由Function Pointers实现Strategy模式
- 上面介绍的NVI方法虽然可以避免客户端直接调用virtual函数&#xff0c;但是在non-virtual函数中还是调用了virtual函数&#xff0c;这种方法还是没有免去定义virtual函数的情况
- 现在我们进行另一种设计&#xff0c;要求每个人物的构造函数接受一个指针&#xff0c;指向一个健康计算函数&#xff0c;我们可以调用该函数进行实际计算。代码如下&#xff1a;
class GameCharacter;//默认的&#xff0c;计算健康指数int defaultHealthCala(const GameCharacter& gc);class GameCharacter {public://函数指针别名typedef int(*HealthCalcFunc)(const GameCharacter& gc);//构造函数explicit GameCharacter(HealthCalaFunc hcf &#61; defaultHealthCalc):healthFunc(hcf) {}int healthValue() {//通过函数指针调用函数return healthFunc(*this);}private:HealthCalcFunc healthFunc; //函数指针};
优点
- ①同一个人物类型之间可以有不同的健康计算函数。例如&#xff1a;
class GameCharacter { //同上 };class EvilBadGuy :public GameCharacter {explicit EvilBadGuy(HealthCalaFunc hcf &#61; defaultHealthCalc):GameCharacter(hcf) {}//..};int loseHealthQuickly(const GameCharacter&);int loseHealthSlowly(const GameCharacter&);int main(){EvilBadGuy ebg1(loseHealthQuickly);EvilBadGuy ebg2(loseHealthQuickly);return 0;}
- ②已定义的对象&#xff0c;在运行期间可以更改健康指数计算函数。例如&#xff0c;可以在类中再添加一个成员函数&#xff0c;用来更改当前计算健康指数的函数指针
此种方法的争议
- ①当全局函数可以根据class的public接口来取得信息并且加以计算&#xff0c;那么这种方法是没有问题的。但是如果计算需要访问到class的non-public信息&#xff0c;那么全局函数就不可以使用了。这个争议将持续到本文的结束
- ②解决上面的问题&#xff0c;唯一方法就是&#xff1a;弱化class的封装。例如将这个全局函数定义为class的friend&#xff0c;或者为其某一部分提供public访问函数
- 因此&#xff0c;这些争议对于“以函数指针替换virtual函数”其是否利大于弊&#xff1f;取决于你的是继续需求
三、藉由tr1::function完成Strategy模式
- “二”中介绍使用全局函数替换成员函数&#xff0c;这种成员函数太过死板&#xff0c;因为“健康指数计算”不必非得是个函数&#xff0c;还可以是其他类型的东西&#xff08;例如函数模板、函数对象等&#xff09;&#xff0c;只要其能计算“健康指数”即可&#xff0c;我们可以使用C&#43;&#43;标准库中的function模板来取代全局函数
- function模板语法参阅&#xff1a;https://blog.csdn.net/qq_41453285/article/details/95184168
- 例如&#xff1a;
class GameCharacter;int defaultHealthCala(const GameCharacter& gc);class GameCharacter {public://其余部分同上//只是将函数指针改为了function模板&#xff0c;其接受一个const GameCharacter&参数&#xff0c;并返回inttypedef std::tr1::function HealthCalcFunc;explicit GameCharacter(HealthCalcFunc hcf &#61; defaultHealthCalc):healthFunc(hcf) {}int healthValue() {return healthFunc(*this);}private:HealthCalcFunc healthFunc;};
使用场景
- function模板的语法知识就不再介绍了。现在我们可以不单单调用全局函数来计算“人物的健康指数”&#xff0c;还可以设计很多种方式来计算
- 现在我们可以自己定义其他方式来计算健康指数。例如&#xff1a;
class GameCharacter { //同上};class EvilBadGuy :public GameCharacter {public:explicit EvilBadGuy(HealthCalcFunc hcf &#61; defaultHealthCalc):GameCharacter(hcf) {}//..};class EyeCandyCharacter :public GameCharacter {//构造函数类似EvilBadGuy};//计算健康指数函数short calcHealth(const GameCharacter&);//函数对象&#xff0c;用来计算健康指数struct HealthCalculator {int operator()(const GameCharacter&)const {}};//其提供一个成员函数&#xff0c;用以计算健康class GameLevel {public:float health(const GameCharacter&)const;};int main(){//人物1&#xff0c;其使用calcHealth()函数来计算健康指数EvilBadGuy ebg1(calcHealth);//人物2&#xff0c;其使用HealthCalculator()函数对象来计算健康指数EyeCandyCharacter ecc1(HealthCalculator());//人物2&#xff0c;其使用GameLevel类的health()成员函数来计算健康指数GameLevel currentLevel;EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1));return 0;}
- 将普通函数替换为function模板&#xff0c;为我们提供了更多可调用物
四、古典的Strategy模式
- 在古典的Strategy设计模式中&#xff0c;会将用来计算健康的函数设计为一个继承体系&#xff0c;并且有virtual函数&#xff0c;这些函数用来计算健康
- UML图如下&#xff0c;意义如下&#xff1a;
- GameCharacter是一个继承体系的根类&#xff0c;其派生类有EvilBadGuy、EyeCandyCharacter
- HealthCalcFunc是一个继承体系的根类&#xff0c;其派生类有SlowHealthLoser、FastHealthLoser
- 每一个GameCharacter对象都内含一个指针&#xff0c;指向于一个来自HealthCalcFunc继承体系中的对象
class GameCharacter;class HealthCalcFunc { //计算健康指数的类public:virtual int calc(const GameCharacter& gc)const {}};HealthCalcFunc defaultHealthCalc;class GameCharacter {public:explicit GameCharacter(HealthCalcFunc* hcf &#61; &defaultHealthCalc):pHealthCalc(hcf) {}int healthValue() {return pHealthCalc->calc(*this);}private:HealthCalcFunc* pHealthCalc;};
五、4种方法的总结
- ①使用non-virtual interface&#xff08;NVI&#xff09;手法&#xff0c;那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性&#xff08;private或protected&#xff09;的virtual函数
- ②将virtual函数替换为“函数指针成员变量”&#xff0c;这是Strategy设计模式的一种分解表现形式
- ③以tr1::function成员变量替换virtual函数&#xff0c;因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式
- ④将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法
六、本文总结
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式
- 将机能从成员函数移到class外部函数&#xff0c;带来的一个缺点是&#xff1a;非成员函数无法访问class的non-public成员
- tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物