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