当前位置: 首页 > article >正文

条款35:考虑虚函数以外的其它选择(Consider alternatives to virtual functions)

条款35:考虑虚函数以外的其它选择

1.1 提出问题

假设正在制作一款游戏,正在为游戏中的角色设计一个层次结构。

class GameCharacter {
public:
    virtual int healthValue() const; //返回角色的生命值;派生类可以重新定义它
    ... // 
};

1.2 解决办法

让我们考虑一些其他的方法来实现同样的效果:

1.2.1 非虚接口

藉由非虚接口(non-virtual interface,NVI)实现 模板方法(Template Method)设计模式:

class GameCharacter {
public:
//healthValue负责包裹doHealthValue
    int healthValue() const // NVI,不允许派生类重新定义
    { 
        ... // 做一些“事前准备”工作(框架的一部分)
            int retVal = doHealthValue(); // 做真正的工作
        ... // 做一些“事后清理”工作(框架的一部分)
            return retVal;
    }
    ...
private:
    virtual int doHealthValue() const // 被隐藏在private里的虚函数,运行重写
    {
        ... // 计算角色生命值的默认算法
    } 
}

如果让客户重写healthValue,则无法确保框架部分的执行。

1.2.2 函数指针

藉由函数指针(Function Pointer)实现策略(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;// 函数指针
};

相同角色类型的不同实例可以具有不同的生命值计算功能。

class EvilBadGuy : public GameCharacter {
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
        : GameCharacter(hcf)//初始化基类部分
    {
        ...
    }
    ...
};
//还可以通过添加成员函数,在运行时改变健康值的计算行为
int loseHealthQuickly(const GameCharacter&); // 健康值计算函数1
int loseHealthSlowly(const GameCharacter&);  // 健康值计算函数2
EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的实例
EvilBadGuy ebg2(loseHealthSlowly); // 具有不同的健康值计算行为

:非成员函数,如果需要访问private成员,需要设置友元或添加接口,会破坏类的封装性。

1.2.3 std::function

藉由 std::function 实现策略(Strategy)设计模式:

class GameCharacter; // 和以前一样
int defaultHealthCalc(const GameCharacter& gc); // 和以前一样
class GameCharacter {
public:
    // HealthCalcFunc是“可调用的实体(callable entity)”
    // 它可以接受任何与GameCharacter兼容的东西,并返回任何与int兼容的东西
    typedef std::function<int(const GameCharacter&)> HealthCalcFunc;

    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
        : healthFunc(hcf)
    {}
    int healthValue() const
    {
        return healthFunc(*this);
    }
    ...
private:
    HealthCalcFunc healthFunc;
};

小小的一个变化,但结果是客户现在在指定生命值计算函数方面具有了惊人的灵活性:

short calcHealth(const GameCharacter&); // 健康值计算函数,返回类型不是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::bind( &GameLevel::health,  currentLevel, _1) 
);

1.2.4 古典的策略(Strategy)设计模式:

使健康值计算函数成为独立的层次结构的虚成员函数,这种方法提供了一种可能性,可以通过向HealthCalcFunc层次结构添加派生类来调整现有的健康值计算算法。
在这里插入图片描述
对应的代码框架:

class GameCharacter; // 前置声明
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;
};

1.3 总结

  1. virtual函数的替代方案包括NVI和策略设计模式的多种形式。NVI本身是一种特殊形式的Template Method设计模式。
  2. 将功能从成员函数移到class外部,带来一个缺点,非成员函数无法访问class的非public成员。
  3. function对象的行为就像一般函数指针。这样的对象可接纳“满足目标签名式”的所有可调用实体。

http://www.kler.cn/a/463664.html

相关文章:

  • 路由基本配置实验
  • 呼叫中心中间件实现IVR进入排队,判断排队超时播放提示音
  • 【HAProxy】如何在Ubuntu下配置HAProxy服务器
  • 【2024年-12月-11日-开源社区openEuler实践记录】深度探秘 libkperf:解锁系统性能剖析的开源宝藏
  • 微信小程序滑动解锁、滑动验证
  • 我用AI学Android Jetpack Compose之开篇
  • 元宇宙金融新纪元:CZ协议全球启航
  • ctrip 小试牛刀记录
  • 分布式系统架构6:链路追踪
  • 基于SpringBoot的题库管理系统的设计与实现(源码+SQL+LW+部署讲解)
  • ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础
  • MySQL 分库分表
  • 对称密码算法(分组密码算法 序列密码算法 密码杂凑算法)中的基本操作
  • 28.Marshal.PtrToStringAnsi C#例子
  • spring网关维度
  • 玩转OCR | 腾讯云智能结构化OCR初次体验
  • vscode 多项目冲突:进行 vscode 工作区配置
  • 【Mars3D项目实战开发】vue3+vite搭建配置项3维地球
  • Redis四种模式在Spring Boot框架下的配置
  • xxxPipeline.from_pretrained(model_path)加载自定义路径下的模型结构
  • 异步背后的奥秘:事件循环
  • K8s中的监控
  • 【智能数据驱动未来】2025年计算机科学技术与机器学习、大数据国际会议 (CSTMLBD 2025)
  • 数组方法 | vue修改数组
  • Docker 安装与配置 Nginx
  • 如何将CSDN文章 导出为 PDF文件