《Effective C++》学习笔记——条款35




六、继承与面向对象设计

six、Inheritance and Object-Oriented Design


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

Rule 35:Differentiate between inheritance of interface and inheritance of implementation





这个条款主要通过一个继承体系来体现:

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

因为不同的人物会有不同的方式来计算,所以,需要将healthValue设置为 virtual 是非常正确的,但它并不是 纯虚函数,所以,我们有个计算健康值的缺省算法。
但是,这样做并不是最完美,它也有缺陷,所以,有没有其他方式来替代呢?
本条款就是介绍下列这些解决的方法。

方法1.借由Non-Virtual Interface 手法实现 Template Method模式

这个方法,主张 virtual函数 应该几乎总是 private。
所以,它们的方法就是 保留healthValue为public,但让它成为 non-virtual,并调用一个 private virtual函数,来进行实际函数

class GameCharacter  {
public:
      int healthValue() const  {    // 派生类 不重新定义它
            ...    // 做一些 其他工作
            int retVal = doHealthValue();
            ...
            return retVal;
      }
  ...
private:
      virtual int doHealthValue() const  {    // 派生类可以重新定义它
            ...    // 缺省算法,可以重新定义
      }
};

让用户通过 public non-virtual成员函数 间接调用 private virtual函数,这种方法称作 non-virtual interface(NVI),也叫作 Template Method设计模式。

  • NVI 方法的 优点 在于 在它做真正的事情前后都可以加入一些其他东西,就像上面定义体现的那样,在执行doHealthValue函数前后,还可以做些别的;但是此手法涉及在派生类中重新定义private virtual函数。
    也就是说,NVI方法 允许 派生类重新定义virtual函数,决定如何去实现;但 基类有着 何时被调用的权利。
  • NVI 方法的 缺点 在于 它没必要让virtual函数一定是private。
    在一些类继承体系中要求派生类在virtual函数实现必须调用其基类的东西,为了让其合法,就不得不让virtual函数为protected甚至要是public,但这么做,就无法实施NVI方法了。


方法2.通过 Function Pointers 实现 Strategy 模式

这个方法主张,健康指数的计算与人物类型无关,因此就不需要这个成分,完全可以让每个人物通过一个函数来自己计算。

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

这个方法就是 Strategy 设计模式的应用。它相较于在人物类内设置virtual函数有几个特性。

  • 同一人物类型不同实体可以有不同的健康计算函数。
  • 某个已知的人物健康指数计算函数可以在运行期变更。

但是,它也有自己的局限性——人物的健康根据该人物的public接口得来的信息加以计算就没问题,但是要用到non-public信息计算,就会出问题。
总之,解决 需要用non-member函数访问class的non-public成分的方法就是 弱化class的封装。例如:
1. class可声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数
2. 运用函数指针替换virtual函数


方法3.通过 tr1::function 完成 Strategy 模式

只要习惯通过templates以及它们对隐式接口的使用,基于函数指针的做法看起来便过分苛刻且死板了。
我们可以不用函数指针,而是用 tr1::function,这个对象可以保存任何 可调用户。

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

HealthCalcFunc是个typedef,用来表现 tr1::function的某个实体,行为像一个函数指针。
它的含义就是 接受一个指向GameCharacter的引用,并返回int。
最后实现起来,就是这样的:

short calcHealth(const GameCharacter&);
struct HealthCalculator  {    // 为计算健康设计的成员对象
    int operator() (const GameCharacter&) const
    {  ...  }
};
class GameLevel  {
public:
    float health(const GameCharacter&) const;    // 计算健康的成员函数
    ...
};
class EvilBadGuy: public GameCharacter  {
    ...        // 同前
};
class EveCandyCharacter: public GameCharacter  {
    ...        // 另一个人物类型,假设构造函数同EvilBadGuy
};
EvilBadGuy edg1(calcHealth);                // 人物1,使用某个函数计算健康指数
EyeCandyCharacter ecc1(HealthCalculator());          // 人物2,使用某个函数对象计算健康指数
GameLevel currentLevel;
...
EvilBadGuy ebg2( std::tr1::bind(&GameLevel::health, currentLevel, _1)               // 人物3,使用某个成员函数计算健康指数
};

这里在 ebg2中第三个参数是_1,它意味着 当为 ebg2调用GameLevel::health时,是通过currentLevel作为GameLevel对象。
但是,这不是重点,重点是,通过tr1::function替换函数指针,就可以允许用户在计算人物健康指数时使用任何兼容的可调用物。


方法4.古典的 Strategy 模式

按方法3的那几个类来讲,通过古典的 Strategy模式来设计,GameCharacter类是某个继承体系的根类,而EvilBadGuy类与EyeCandyCharacter类都是派生类;而 HealthCalcFunc是另一个继承体系的根类,在这个体系也有一些其他的派生类;每一个GameCharacter对象都内含一个指向HealthCalcFunc继承体系对象的指针。

class GameCharacter;
class HealthCalcFunc  {
public:
  ...
  virtual int clac(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;
};

这个方法对于熟悉标准Strategy模式的人易理解使用,而且它还提供了 将一个既有的健康算法纳入使用的可能性。也就是在HealthCalcFunc继承体系中加派生类。



请记住

  • virtual函数的替代方案包括 NVI方法 及 Strategy设计模式的多种形式。NVI方法 自身是一个特殊形式的 Template Method 设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class内的 non-public成员。
  • tr1::function 对象的行为就像一般的函数指针。这样的对象可接纳“与给定的目标签名式兼容”的所有可调用物。




你可能感兴趣的:(学习笔记)