假设我们在写一个3D游戏软件,打算为游戏内的人物设计一个继承体系。游戏内容属于暴力砍杀类型,游戏中的角色被伤害或其它因素导致健康状态下降的情况是一个常见属性。因此设计一个成员函数healthValue,它会返回一个整数,表示人物的健康程度。
由于不同的任务可能以不同的方式计算它们的健康指数,将healthValue声明为virtual似乎是最直白的做法:
class GameCharacter{
public:
virtual int healthValue() const;//返回人物的健康指数
....
};
在这里,healthyValue并未被声明未pure virtual,这暗示我们将会有个计算健康指数的缺省算法。这的确是个很直白的设计,但是从某个角度说却反而成为它的弱点了。由于这个设计太过于明显,导致我们可能都没有认真考虑过其它的替代方案,这里我们考虑用其它方案来替代。
由Non-Virtual Interface手法实现Template Method模式
首先我们先从一个思想流派说起,该流派认为virtual函数应该几乎总是private。该流派认为:较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(比如doHealthValue)进行实际工作:
class GameCharacter{
public:
int healthValue() const
{
...
int retVal=doHealthValue();
...
return retVal;
}
...
private:
virtual int doHealthValue() const//derived classes可重新定义该函数
{
...//缺省算法,计算健康指数
}
};
在这段代码中,我直接在class定义式内呈现函数本体。
这一基本设计,也就是”令客户通过public non-virtual成员函数间接调用private virtual函数“,称该方法为NVI(non-virtual interface)手法。它是所谓template Method设计模式的一个独特表现形式,我把这个non-virtual函数(healthValue)称为virtual函数的外覆器
该方法的一个优点是在上述代码注释”做一些事前工作“和”做一些事后工作“之中。那些注释用来告诉你当时的代码保证在”virtual函数进行真正工作之前和之后被调用“。这意味着外覆器(wrapper)确保得以在一个virtual函数被调用之前设定好适当的场景,并在调用结束之后清理场景。”事前工作“可以包括锁定互斥器(locking a mutex),制造运转日志记录项(log entry),验证class约束条件,验证函数先决条件。”事后工作“可以包括互斥器解锁锁定(unlocking a mutex),验证函数的事后条件,再次验证class约束条件等等,倘如让客户直接调用virtual函数,就没有啥办法做这些事情了。
NVI方案涉及在derived classes内重新定义private virtual函数。”重新定义virtual函数“表示某些事如何被完成,”调用virtual函数“表示其什么时候被完成。这些事情都是各自独立互不相关的。NVI方案允许derived classes重新定义virtual函数,但base class保留”函数何时被调用“的权利。
在NVI方案下其实没有必要让vitual函数一定得是private,某些class继承体系要求derived class在virtual函数的实现内必须调用base class的对应函数,而为了让这样的调用合法,virtual函数必须是protected,不能是private。有时候virtual函数甚至一定是public,这种情况下,就不能实施NVI方案了。
籍由Function Pointers实现Strategy模式
NVI方案对public virtual函数而言是一个有趣的替代方案,但从某种设计角度来说,它还没脱离virtual的本质。毕竟我们还是使用virtual函数来计算每个人物的健康指数。
这里有另外一种设计方案,该方案主张”人物健康指数的计算与人物类型无关“,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接收一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
class GameCharacter;//前置声明
//以下是函数计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
typedef int(*HealthCalcFunc)(const GameCharacter&);
explict GameCharacter(HealthCalcFunc hcf=defaultHealthCalc):healthFunc(hcf)
{ }
int healthValue() const
{
return healthFunc(*this);
}
....
private:
HealthCalcFunc healthFunc;
};
这种做法是常见的Strategy设计模式的简单应用,它与GameCharacter继承体系内的virtual函数相比,具有以下这些特点:
同一人物类型的不同实体可以有不同的健康计算函数,比如:
class EvilBadGuy:public GameCharacter{
public:
explict EvilBadGuy(HealthCalcFunc hcf=defaultHealthCalc):GameCharacter(hcf)
{
...
}
};
int loseHealthQuickly(const GameCharacter&);//健康指数计算函数1
int loseHealthSlowly(const GameCharacter&);//健康指数计算函数2
EvilBadGuy ebg1(loseHealthQuickly);//相同类型的人物搭配
EvilBadGuy ebg2(loseHealthSlowly);//不同的健康计算方式
若已知人物的健康计算函数可以在运行期间变更,例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
换句话说,健康指数计算函数不再是GameCharacter继承体系内的成员函数,这一事实意味着计算函数并未特别访问“即将被计算健康指数”的那个对象的内部成分。例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。
如果人物的健康可单纯根据该人物public接口得来的信息加以计算,这就没有问题,但如果需要non-public信息进行精确计算,就有问题了。实际上任何时候当你将class内的某个机能(也许来自某个成员函数)替换为class外部的某个等价机能(也许取到自某个non-member non-friend函数或另一个class的non-friend成员函数),这都是潜在争议点。
一般而言,唯一能够解决“需要以non-member函数访问class的non-public成分”的办法就是:弱化class的封装,比如class可声明为non-member函数为friends,或是为其实现的某一部分提供public访问函数。运用函数指针替换virtual函数。
籍由trl::function完成Strategy模式
一旦习惯了templates以及它们对隐式接口的使用,基于函数指针的做法看起来就显得过于死板了。这里有个问题,为什么一定得是函数,为什么不能够是个成员函数,为什么一定得返回Int而不是任何可被转换为int得类型呢?
假设我们不再使用函数指针(如前面得healthFunc),而是改用一个类型为trl::function的对象,这些约束就全部消失不见了。见以下例子:
trl::function:
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
//HealthCalcFunc可以是任何“可调用物”,可被调用并接受
//任何兼容于GameCharacter之物,返回任何兼容于int的东西
typedef std::trl::function HealthCalcFunc;
explict GameCharacter(HealthCalcFunc hcf=defaultHealthCalc):healthFunc(hcf)
{ }
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
如你所见,HealthCalcFunc是个typedef,用来表现trl::function的某个具现化,意味着该具现化的行为像一个一般的函数指针。现在我们靠近一点瞧瞧HealthCalcFunc是个什么样的typedef:
HealthCalcFunc是个什么样的typedef:
std::trl::function
这里我把trl::function具现体的目标签名以不同的颜色强调出来。那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”。这个trl::function类型(即HealthCalcFunc)产生的对象可以持有任何与此签名式兼容的可调用物。所谓兼容,即这个可调用物的参数可以被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。
和前一个设计比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个trl::function对象,相当于一个指向函数的泛化指针。见下例子:
short calcHealth(const GameCharacter&);//健康计算函数
struct HealthCalculator{//为计算健康而设计的函数对象
int operator() (const GameCharacter&) const
{...}
};
class GameLevel{
public:
float health(const GameCharacter&) const;//成员函数,用以计算健康
...//注意其non-int返回类型
};
class EvilBadGuy:public GameCharacter{
...
};
class EyeCandyCharacter:public GameCharacter{//另外一个人物类型,假设其构造函数与EvilBadGuy同
....
};
EvilBadGuy ebg1(calcHealth);//人物1,使用某个函数计算健康指数
EyeCandyCharacter eccl(HealthCalculator())//人物2,使用某个函数对象计算健康指数
GameLevel currentLevel;
...
EvilBadGuy ebg2(std::trl::bind(&GameLevel::health,currentLevel,_l));//人物3,使用某个成员函数计算健康指数
就我个人而言,当我发现trl::function允许我们做的事时非常惊讶。
首先我要表明,为计算ebg2的健康指数,应该使用GameLevel class的成员函数health,GameLevel::health宣称它自己接受一个参数(那是reference 指向GameCharacter),但实际上它接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter,如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数,转而接受单一参数(一个GameCharacter)。
本条款的根本忠告是当你为解决问题寻找某个设计方法时,不妨考虑virtual函数的替代方案。
1.使用non-virtual interface方案,那是Template method设计模式的一种特俗形式。它以public non-virtual成员函数包裹较低访问性的virtual函数
2.将virtual函数替换为“函数指针成员变量”,这是strategy设计模式的一种分解表现形式
3.以trl::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式
4.将继承体系内的virtual函数替换为另一个继承体系内的virtual函数,这是strategy设计模式的传统实现方法。