C++之考虑virtual函数以外的其他选择(35)---《Effective C++》

xiaoxiao2021-02-28  16

条款35:考虑virtual函数以外的其他选择

我们想要编写一个游戏软件,为游戏内部的任务设计一个继承体系,我们需要计算每个任务的血量,因此设计成员函数healthValue,由于不同人物的血量计算方式不同,因此需要将healthValue声明为virtual啦!healthValue并未被声明为pure virtual,暗示healthValue方法有默认的缺省算法。

class GameCharacter{ public: virtual int healthValue() const; ... };

这是最简单的设计,但可能成为它的弱点,由于这个设计如此明显,可能并不会认真考虑完整的替代方案,为了扩展思路,跳脱常轨,让我们考虑一些其他算法!

摘要: 1)使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式,它以public non-virtual成员函数包裹较低访问性(private或者protected)的virtual函数; 2)将virutal函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式; 3)以trl::function成员变量替换virtual函数,因而允许使用任何可调用误(callable entity)搭配一个兼容与需求的签名式,这也是Strategy设计模式的某种形式; 4)将继承体系内的virtual函数替换为另一个继承体系中的virtual函数,这是Strategy设计模式的传统实现手法。

1. 借由Non-Virtual Interface(NVI)手法实现Templated Method模式

class GameCharacter{ public: int healthValue()const{ ... int retVal=doHealthValue(); ... return retVal; } ... private: virtual int doHeatlthValue() const{ ... } };

这种设计,“令用户通过public non-virtual方法调用private virtual函数”,称为non-virtual interface(NVI)手法,这是所谓的Template Method设计模式(与C++ templates并无关联)的一个独特表现形式,我把这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。

2. 借由Function Pointer实现Strategy模式

class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter{ public: typedef int(*HealthCalcFunc) (const GameCharacter); explicit GameCharacter(HealthCalcFunc hfc=defaultHealthCalc):health(hcf) {} int healthValue() const{ return healthFunc(*this); } ... private: HealthCalcFunc healthFunc; };

这个做法是Strategy设计模式的额简单引用,拿它和“植基于GameCharacter继承体系之virtual函数”的做法比较,它提供了某些有趣弹性: 1)同一人物类型之间的不同实体可以有不同的健康计算函数,如:

class EvilBadGuy:public GameCharacter{ public: explicit EvilBadGuy(HealthCalcFunc hcf=defaultHealthCalc):GameCharacter(hcf) {...} ... }; int loseHealthQuickly(const GameCharacter&); int loseHealthSlowly(const GameCharacter&); EvilBadGuy ebg1(loseHealthQuickly); EvilBadGuy ebg2(loseHealthSlowly);

2)某已知人物之甲亢支书计算函数可在运行期变更,例如GameCharacter可以提供一个成员函数setHealthCalculator,用来替换当前的支书计算函数。

唯一能够解决“需要以non-member函数访问class 的non-public成分”的办法就是:弱化class的封装。例如class可以声明那个non-member函数为friends,或是为其实现的某一部分提供的public访问函数(其他部分则宁可各自拥有自己的健康计算函数和“可在运行期间改变的计算函数”)是否足以弥补缺点(例如可能必须降低GameCharacter的封装性),是你必须更具每个设计情况的不同而需要加以抉择的。

3. 借由trl::function完成Strategy模式

一旦习惯了templates以及他们对隐式接口的使用,基于函数指针的做法看起来便过分苛刻死板了,为什么要求“HealthCalcFunc”必须是个函数,而不能是某种“像函数的东西”呢?如果一定需要是函数,威慑么不能够是个成员函数?威慑么一定得返回int而不是任何可以被转换为int的类型呢? 如果我们不再使用函数指针,改用一个类型为trl::function的对象那个,这些约束就全都会发不见了,这样的对象可以持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象或者成员函数指针),只要其签名式兼容于需求端,以下将刚才的设计改为使用trl::function:

class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter{ public: typedef std::trl::function<int (const GameCharacter&> HealthCalcFunc; explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc):heathFunc(hcf) {} int healthValue() const { return healthFunc(*this); } ... private: HealthCalcFunc healthFunc; };

此时HealthCalcFunc是个typedef,用来表现trl::function这个具现体,意味着具现体的行为像一般的函数指针,现在让我们瞅瞅HealthCalcFunc是个怎样的typedef:

std::trl::function<int (const GameCharacter&)>

这里我们将trl::function具现体的目标签名式(target signature)以不同的颜色强调出来,这个签名代表的函数是“接受一个reference指向const的GameCharacter,并返回int”,这个trl::function类型(也就是这里的HealthCalcFunc类型)产生的对象那个可以保存任何与此签名式兼容的可调用物(callable entity,即函数指针、函数引用或者成员函数引用),所谓兼容,意思就是这个可调用误的参数可以被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。这种设计相较与前一个设计只是让GameCharacter保存一个trl::function对象,相当于一个指向函数的泛华指针,这个改变如此细小,我总说它没有任何外显影响,除非客户在“指定健康计算函数”这件事情上面需要更惊人的弹性。

short calcHeatlth(const GameCharacter&); struct HealthCalculator{ int operator()(const GameCharacter&) const { ... } }; class GameLevel{ public: float health(const GameCharacter&) const; ... }; class EvilBadGuy:public GameCharacter{ ... }; class EyeCandyCharacter:public GameCharacter{ ... }; EvilBadGuy ebg1(calcHealth); EyeCandyCharacter eccl(healthCalculator()); GameLevel currentLevle; ... EvilBadGuy ebg2(std::trl::bind(&GameLevel::health,currentLevle,_1));

4. 古典的Strategy模式

上图表示的是GameCharacter作为基类,EvilBadGuy和EyeCandyCharacter作为GameCharacter的派生类,同时HealthCalcFunc作为基类,SlowHealthLoser和FastHealthLoser作为HealthCalcFunc,然后GameCharacter中有一个HealthCalcFunc指针的成员变量,看看如下代码:

class GameCharacter; 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模式的人很容易辨认它,而且它还提供“讲一个即有的健康算法纳入使用”的可能性—只要为HealthCalc继承体系中添加一个derived class即可。

总结:

1)virtual函数的替代方案包括NVI手法以及Strategy设计模式的多种形式,NVI手法自身是一个特殊形式的Template Method设计模式; 2)将机能从成员函数转移到class外部函数,带来的一个缺点是:非成员函数无法访问class 的non-public成员; 3)trl::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物(callable entities)。

转载请注明原文地址: https://www.6miu.com/read-1099986.html

最新回复(0)