The Strategy Pattern via tr1::function(经由 tr1::function 实现的策略模式)
一旦你习惯了 templates(模板)和 implicit interfaces(隐式接口)(参见 Item 41)的应用,function-pointer-based(基于函数指针)的方法看上去就有些死板了。健康值的计算为什么必须是一个 function(函数),而不能是某种简单的行为类似 function(函数)的东西(例如,一个 function object(函数对象))?如果它必须是一个 function(函数),为什么不能是一个 member function(成员函数)?为什么它必须返回一个 int,而不是某种能够转型为 int 的类型?
如果我们用一个 tr1::function 类型的对象代替一个 function pointer(函数指针)(诸如 healthFunc),这些约束就会消失。就像 Item 54 中的解释,这样的对象可以持有 any callable entity(任何可调用实体)(例如,function pointer(函数指针),function object(函数对象),或 member function pointer(成员函数指针)),这些实体的标志性特征就是兼容于它所期待的东西。我们马上就会看到这样的设计,这次使用了 tr1::function:
class GameCharacter; // as before
int defaultHealthCalc(const GameCharacter& gc); // as before
class GameCharacter {
public:
// HealthCalcFunc is any callable entity that can be called with
// anything compatible with a GameCharacter and that returns anything
// compatible with an int; see below for details
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};
就像你看到的,HealthCalcFunc 是一个 tr1::function instantiation(实例化)的 typedef。这意味着它的行为类似一个普通的 function pointer(函数指针)类型。我们近距离看看 HealthCalcFunc 究竟是一个什么东西的 typedef:
std::tr1::function<int (const GameCharacter&)>
这里我突出了这个 tr1::function instantiation(实例化)的“target signature(目标识别特征)”。这个 target signature(目标识别特征)是“取得一个引向 const GameCharacter 的 reference(引用),并返回一个 int 的函数”。这个 tr1::function 类型的(例如,HealthCalcFunc 类型的)对象可以持有兼容于这个 target signature(目标识别特征)的 any callable entity(任何可调用实体)。兼容意味着这个实体的参数能够隐式地转型为一个 const GameCharacter&,而它的返回类型能够隐式地转型为一个 int。
与我们看到的最近一个设计(在那里 GameCharacter 持有一个指向一个函数的指针)相比,这个设计几乎相同。仅有的区别是目前的 GameCharacter 持有一个 tr1::function 对象——指向一个函数的 generalized(泛型化)指针。除了达到“clients(客户)在指定健康值计算函数时有更大的灵活性”的效果之外,这个变化是如此之小,以至于我宁愿对它视而不见:
short calcHealth(const GameCharacter&); // health calculation
// function; note
// non-int return type
struct HealthCalculator { // class for health
int operator()(const GameCharacter&) const // calculation function
{ ... } // objects
};
class GameLevel {
public:
float health(const GameCharacter&) const; // health calculation
... // mem function; note
}; // non-int return type
class EvilBadGuy: public GameCharacter { // as before
...
};
class EyeCandyCharacter: public GameCharacter { // another character
... // type; assume same
}; // constructor as
// EvilBadGuy
EvilBadGuy ebg1(calcHealth); // character using a
// health calculation
// function
EyeCandyCharacter ecc1(HealthCalculator()); // character using a
// health calculation
// function object
GameLevel currentLevel;
...
EvilBadGuy ebg2( // character using a
std::tr1::bind(&GameLevel::health, // health calculation
currentLevel, // member function;
_1) // see below for details
);
就个人感觉而言:我发现 tr1::function 能让你做的事情是如此让人惊喜,它令我浑身兴奋异常。如果你没有感到兴奋,那可能是因为你正目不转睛地盯着 ebg2 的定义并对 tr1::bind 的调用会发生什么迷惑不解。请耐心地听我解释。
比方说我们要计算 ebg2 的健康等级,应该使用 GameLevel class(类)中的 health member function(成员函数)。现在,GameLevel::health 是一个被声明为取得一个参数(一个引向 GameCharacter 的引用)的函数,但是它实际上取得了两个参数,因为它同时得到一个隐式的 GameLevel 参数——指向 this。然而,GameCharacters 的健康值计算函数只取得单一的参数:将被计算健康值的 GameCharacter。如果我们要使用 GameLevel::health 计算 ebg2 的健康值,我们必须以某种方式“改造”它,以使它适应只取得唯一的参数(一个 GameCharacter),而不是两个(一个 GameCharacter 和一个 GameLevel)。在本例中,我们总是要使用 currentLevel 作为 GameLevel 对象来计算 ebg2 的健康值,所以每次调用 GameLevel::health 计算 ebg2 的健康值时,我们就要 "bind"(凝固)currentLevel 来作为 GameLevel 的对象来使用。这就是 tr1::bind 的调用所做的事情:它指定 ebg2 的健康值计算函数应该总是使用 currentLevel 作为 GameLevel 对象。
我们跳过一大堆的细节,诸如为什么 "_1" 意味着“当为了 ebg2 调用 GameLevel::health 时使用 currentLevel 作为 GameLevel 对象”。这样的细节并没有什么启发性,而且它们将转移我所关注的基本点:在计算一个角色的健康值时,通过使用 tr1::function 代替一个 function pointer(函数指针),我们将允许客户使用 any compatible callable entity(任何兼容的可调用实体)。很酷是不是?
The "Classic" Strategy Pattern(“经典的”策略模式)
如果你比 C++ 更加深入地进入 design patterns(设计模式),一个 Strategy 的更加习以为常的做法是将 health-calculation function(健康值计算函数)做成一个独立的 health-calculation hierarchy(健康值计算继承体系)的 virtual member function(虚拟成员函数)。做成的 hierarchy(继承体系)设计看起来就像这样:
如果你不熟悉 UML 记法,这不过是在表示当把 EvilBadGuy 和 EyeCandyCharacter 作为 derived classes(派生类)时,GameCharacter 是这个 inheritance hierarchy(继承体系)的根;HealthCalcFunc 是另一个带有 derived classes(派生类)SlowHealthLoser 和 FastHealthLoser 的 inheritance hierarchy(继承体系)的根;而每一个 GameCharacter 类型的对象包含一个指向“从 HealthCalcFunc 派生的对象”的指针。
这就是相应的框架代码:
class GameCharacter; // forward declaration
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 pattern(策略模式)实现的人可以很快地识别出来,再加上它提供了通过在 HealthCalcFunc hierarchy(继承体系)中增加一个 derived class(派生类)而微调已存在的健康值计算算法的可能性。
Summary(概要)
这个 Item 的基本建议是当你为尝试解决的问题寻求一个设计时,你应该考虑可选的 virtual functions(虚拟函数)的替代方法。以下是对我们考察过的可选方法的一个简略的回顾:
- 使用 non-virtual interface idiom (NVI idiom)(非虚拟接口惯用法),这是用 public non-virtual member functions(公有非虚拟成员函数)包装可访问权限较小的 virtual functions(虚拟函数)的 Template Method design pattern(模板方法模式)的一种形式。
- 用 function pointer data members(函数指针数据成员)代替 virtual functions(虚拟函数),一种 Strategy design pattern(策略模式)的显而易见的形式。
- 用 tr1::function data members(数据成员)代替 virtual functions(虚拟函数),这样就允许使用兼容于你所需要的东西的 any callable entity(任何可调用实体)。这也是 Strategy design pattern(策略模式)的一种形式。
- 用 virtual functions in another hierarchy(另外一个继承体系中的虚拟函数)代替 virtual functions in one hierarchy(单独一个继承体系中的虚拟函数)。这是 Strategy design pattern(策略模式)的习以为常的实现。
这不是一个可选的 virtual functions(虚拟函数)的替代设计的详尽无遗的列表,但是它足以使你确信这些是可选的方法。此外,它们之间互为比较的优劣应该使你考虑它们时更为明确。
为了避免陷入 object-oriented design(面向对象设计)的习惯性道路,时不时地给车轮一些有益的颠簸。有很多其它的道路。值得花一些时间去考虑它们。
Things to Remember
- 可选的 virtual functions(虚拟函数)的替代方法包括 NVI 惯用法和 Strategy design pattern(策略模式)的各种变化形式。NVI 惯用法本身是 Template Method design pattern(模板方法模式)的一个实例。
- 将一个机能从一个 member function(成员函数)中移到 class(类)之外的某个函数中的一个危害是 non-member function(非成员函数)没有访问类的 non-public members(非公有成员)的途径。
- tr1::function 对象的行为类似 generalized function pointers(泛型化的函数指针)。这样的对象支持所有兼容于一个给定的目标特征的 callable entities(可调用实体)。