• 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即可。

  • 相关阅读:
    Vagrant 扩大磁盘根目录
    阿里云 轻量应用服务器 vnc 远程桌面连接
    图片加水印C#源代码
    Asp.net网站Pdf加水印C#源代码
    [FAQ] uni-app 如何让页面不展示返回箭头图标
    [PHP] composer, PHP Fatal error: Allowed memory size of xx bytes exhausted
    [FE] uni-app 导航栏开发指南
    [FE] uni-app 动态改变 navigationBarTitleText 导航标题
    [FE] yarn, npm 切换镜像源
    [FAQ] Phpstorm 代码提示功能失效问题
  • 原文地址:https://www.cnblogs.com/lidan/p/2346172.html
Copyright © 2020-2023  润新知