36.考虑virtual以外的选择

(本节虽然带有面向对象标签,但描述的是如何跳出面向对象的思维看待问题)

问题实例

 
假定我们在编写一款游戏,其中的角色带有一个血条(int),不同的人计算血量的方式不同,所以把该函数声明为virtual似乎很合适:

1
2
3
4
5
class GameCharacter{
public:
virtual int healthValue() const;
...
};

该函数并未定义为pure virtual,这说明我们有一个缺省方法。
让我们现在跳出面向对象设计的常规轨道,考虑一些其他的实现可能。


Template Method 模式(Non-virtual Interface)

有一些人主张virtual函数必须全是private。以实际问题举例,上个例子保留healthvalue为public non-virtual成员函数,并调用一个private virtual函数来进行实际操作。

1
2
3
4
5
6
7
8
9
10
11
12
class GameCharacter{
public:
int healthValue() const{
...//事前处理
int retVal = getHealthValue();
...//事后处理
return retVal;
}
...
private:
virtual int getHealth() const {...}
};

这一基本设计被称为non-virtual interface(NVI)手法。它是所谓template method设计模式(与c++的templates无关)的独特表现形式。我们习惯于把non-virtual函数称为virtual函数的外覆器(wrapper)。

NVI的优点

NVI手法的一大优点在于wrapper确保了virtual函数在调用前的场景已被设计好,并且调用结束后场景得到了清理。(实例中的注释处)举例而言,这些工作可能包括:锁定互斥器、记录日志、验证约束条件、互斥器解锁等等。如果你让客户直接调用virtual,那么可能没办法做好这些事。
在这里需要注意的是:NVI手法涉及在dc内重新定义了private virtual函数。重定义virtual函数表示“某事应当被如何完成”,外覆器的non-virtual属性则表示“某事应当何时完成”(函数何时被调用),这二者并不冲突。

NVI的不足

在NVI手法下不一定virtual一定得是private,它们也可以是portected.(详见Effective C++ 28 Windows实例)有时候virtual甚至必须是public(具有多态性质的bc的析构函数),这么一来就不能实施NVI手法了。


Strategy模式(Function Pointers)

 
NVI手法本质上还是在用virtual函数,只是调用时颇为巧妙而已。针对游戏实例,另有一些人主张“人物血条的计算与人物类型无关”,这种计算不需要“人物”成分。
例如我们可能会要求每一个人物构造函数接受一个指针指向血条计算函数,我们调用该函数进行实际操作:

1
2
3
4
5
6
7
8
9
class GameCharcter;
int defaultHealthCalc(const GameCharacter &gc);//默认计算函数
class GameCharacter
public:
typedef int (*HealthCalc)(const GameCharacter&);
explicit GameCharcter(HealthCalc hc = defaultHealthCalc):HealthFunc(hc) {}
private:
HealthCalc healthFunc;
}

Strategy的优点

上述做法是strategy设计模式的一个简单应用,和普通的virtual函数相比,其优点如下:

  • 同一人物类型的不同实体可以有不同的计算函数(构造时绑定不同的函数指针)。
  • 血条计算函数可以在人物的不同时期发生变更(比如设置个setHealthcalc函数接受一个函数指针并且替换私有成员)。

Strategy的缺点

在Strategy模式下,血条计算函数已经不再是gamecharacter继承体系内的成员函数,也就是说,它将无法访问对象的non-public部分。
解决方法只有弱化class的封装:

  • 将函数声明为friends
  • 为其需要的数据提供一个public访问接口

至于strategy模式的优点(每个对象拥有各自的函数以及动态更改的能力)能否抵消缺点(降低class的封装程度),必须根据设计情况的不同进行抉择。


Strategy模式(Function)

 
为什么血条计算必须得是一个函数呢?它完全可以是一个类似函数的对象。如果我们使用function对象,很多刚才提及的约束一下子就消失了。funcion对象持有一切可调用物(函数指针、函数对象、成员函数指针),因此刚才的设计可以改为:

1
2
3
4
5
6
7
8
9
10
11
12
class GameCharcter;
int defaultHealthCalc(const GameCharacter &gc);//默认计算函数
class GameCharacter
public:
function<int (const GameCharacter&)> healthCalcFunc;
explicit GameCharcter(healthCalcFunc hcf = defaultHealthCalc):HealthFunc(hcf) {}
int getHealthValue() const {
return healthFunc(*this);
}
private:
healthCalcFunc healthFunc;
}

和前一个相比,这个设计几乎完全一样,唯一的不同之处是如今class持有的是一个function对象,相当于一个指向函数的泛化指针。然而这个改变为客户带来了更加惊人的弹性(原书中使用了bind增加了形参)也就是说,如今的设计允许客户在计算人物健康指数时使用任何兼容的可调用物。


传统Strategy模式
 
传统Strategy模式更倾向于将当前virtual函数做成一个分离的继承体系中的virtual成员函数,其设计结果大概像这样:
image_1cbc0cklns971g0h14mt1fed1h3s9.png-24.5kB
其核心在于GameCharacter与HealthcalcFunc成为了base class,并且Gamecharacter中含有一个指针,指向HealthcalcFunc继承体系中的对象。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class GameCharacter;
class HealthCalcFunc{
public:
...
virtual int calc(const GameCharacter& gc) const {...};
...
}
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
:pHealthC(phcf) {}
int healthValue() const{
return pHealthCalc0>calc(*this);
}
...
private:
HealthCalcFunc* pHealthCalc;
};


总结

 
当你为解决当前问题而寻找设计方法时,不妨考虑virtual的以下几个替代方案:

  • NVI手法,template method设计模式的特殊形式。
    它以public non-virtual成员函数包裹较低访问性(private、protected)的virtual函数。
  • 将virtual函数替换为函数指针
  • 将函数指针变成function对象,以上两种都是strategy的某种表现形式。
  • 将继承体系内的virtual换成另一个继承体系内的virtual,这是strategy设计模式的传统实现。

面向对象固然博大精深,但这个世界上还有许多条路值得我们去探索。