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

假设你整在写一个视频游戏软件,由于不同的人物可能以不同的方式计算它们的健康指数,将healthValue声明为virtual似乎再明白不过的做法:

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

由于这个设计如此明显,你可能没有认真考虑其他替代方案。为了帮助你跳脱面向对象设计路上的常轨,让我们考虑其他一些解法:

藉由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 class 可以重新定义它。
    {
        ...//缺省计算,计算健康指数
    }
};

这一基本设计,“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。是所谓template Method设计模式的一个独特表现形式。把这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。

NVI的优点在上述代码注释“做一些事前工作”和“做一些事后工作”之中。这意味着外覆器(wrapper)确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。“事前工作”可以包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等。

NVI手法涉及在derived class内重新定义private virtual函数。重新定义若干个derived class并不调用的函数!这里并不矛盾。“重新定义virtual函数”表示某些事“如何”被完成,“调用virtual函数”则表示它“何时”被完成。NVI允许derived class重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权力。

NVI手法其实没必要让virtual函数一定是private。有时必须是protected。还有时候甚至是public,这么一来就不能实施NVI手法了。

藉由Function Pointers实现Strategy模式

另一个设计主张是“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

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

这个做法是常见的Strategy设计模式的简单应用。和“GameCharacter继承体系内的virtual函数”的做法比较,它提供了某些有趣弹性:

同一人物类型不同实体可以有不同的健康计算函数。例如:

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

EvilBadGuy ebg1(loseHealthSlowly);//相同类型的人物搭配
EvilBadGuy ebg2(loseHealthQuickly);//不同的健康计算方式

某已知人物健康指数计算函数可以在运行期变更:例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

这些计算函数并未特别访问“即将被计算健康指数”的那个对象内部成分。例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。如果需要non-public信息进行精确计算,就有问题了。唯一能解决“需要以non-member函数访问class的non-public成分”的办法就是:弱化class的封装。例如class可以声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数。利用函数指针替换virtual函数,其优点(像是“每个对象可各自拥有自己的健康计算函数”和“可在运行期间改变计算函数”)是足以弥补缺点(例如可能必须降低GameCharacter封装性)。

藉由tr1::function完成Strategy模式

我们不再用函数指针,而是用一个类型为tr1::function的对象,这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象、或成员数函数指针),只要其签名式兼容于需求端。

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
    //HealthCalcFunc可以是任何“可调用物”,可被调用并接受
    //任何兼容于GameCharacter之物,返回任何兼容于int的东西。
    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
        : healthFunc(hcf)
    {}
    int healthValue() const
    {
        return healthFunc(*this);
    }
    ...
private:
    HealthCalcFunc healthFunc;
};

这里我们把tr::function的具现体的目标签名式以不同颜色强调出来。那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”

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

所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换成int。

客户在“指定健康计算函数”这件事上有更惊人的弹性:

short calcHealth(const GameCharacter&); //函数return non-int
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 ecc1(HealthCalculator());//函数对象
GameLevel currentLevel;
...
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1));//成员函数

GameLevel::health宣称它接受两个参数,但实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter和一个GameLevel),转而接受单一参数(GameCharacter)。于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为。

古典的Strategy模式

将健康计算函数做成一个分离的继承体系中的virtual成员函数。

class GameCharacter;
class HealthCalcFunc {
    ...
    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;
};

每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。

还可以提供“将一个既有的健康计算算法纳入使用”的可能性--只要为HealthCalcFunc继承体系添加一个derived class即可。

posted @ 2012-02-10 22:32  lidan  阅读(594)  评论(0编辑  收藏  举报