条款 35:考虑 virtual 函数以外的其他选择(一)

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 继承体系的对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 《Effective C++ 中文版 第三版》读书笔记 某已知人物健康指数计算函数可以在运行期变更:例如 Game...
    赵者也阅读 207评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 本原则所叙述的内容比较复杂涉及到设计模式相关的内容。它主要讲的就是以non-virtual函数代替virtual函...
    Stroman阅读 600评论 0 1
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • 夜色的灯光还没有黯去,如一条寂寞的长龙,独自在江面上横卧着。 暗影中,台风带起云层,快速的向北推进。推到北山之颠,...
    风儿轻轻阅读 234评论 0 4