Effective C++条款35:继承与面向对象——考虑virtual函数以外的其他选择

  • 假设现在有这样的一个程序:
    • 正在写一个视频游戏软件,现在为游戏内的任务设计一个继承体系
    • 现在我们定义一个函数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对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值