C++ 明智地使用多继承

要看是谁来说,多继承(MI)要么被认为是神来之笔,要么被当成是魔鬼的造物。支持者宣扬说,它是对真实世界问题进行自然模型化所必需的;而批评者争论说,它太慢,难以实现,功能却不比单继承强大。更让人为难的是,面向对象编程语言领域在这个问题上至今仍存在分歧:C++,Eiffel和the Common LISP Object System (CLOS)提供了MI;Smalltalk,Objective C和Object Pascal没有提供;而Java只是提供有限的支持。可怜的程序员该相信谁呢?

在相信任何事情之前,首先得弄清事实。C++中,关于MI一条不容争辩的事实是,MI的出现就象打开了潘朵拉的盒子,带来了单继承中绝对不会存在的复杂性。其中,最基本的一条是二义性(参见技巧26)。如果一个派生类从多个基类继承了一个成员名,所有对这个名字的访问都是二义的;你必须明确地说出你所指的是哪个成员。下面的例子取自ARM(参见技巧50)中的一个专题讨论:

  1. class Lottery {
  2. public:
  3.   virtual int draw();
  4.   ...
  5. };
  6. class GraphicalObject {
  7. public:
  8.   virtual int draw();
  9.   ...
  10. };
  11. class LotterySimulation: public Lottery,
  12.                          public GraphicalObject {
  13.   ...                          // 没有声明draw
  14. };
  15. LotterySimulation *pls = new LotterySimulation;
  16. pls->draw();                   // 错误! ---- 二义
  17. pls->Lottery::draw();          // 正确
  18. pls->GraphicalObject::draw();  // 正确
复制代码

这段代码看起来很笨拙,但起码可以工作。遗憾的是,想避免这种笨拙很难。即使其中一个被继承的draw函数是私有成员从而不能被访问,二义还是存在。(对此有一个很好的理由来解释,但完整的说明在技巧26中提供,所以此处不再重复。)

显式地限制修饰成员不仅很笨拙,而且还带来限制。当显式地用一个类名来限制修饰一个虚函数时,函数的行为将不再具有虚拟的特征。相反,被调用的函数只能是你所指定的那个,即使调用是作用在派生类的对象上:

  1. class SpecialLotterySimulation: public LotterySimulation {
  2. public:
  3.   virtual int draw();
  4.   ...
  5. };
  6. pls = new SpecialLotterySimulation;
  7. pls->draw();                        // 错误! ---- 还是有二义
  8. pls->Lottery::draw();               // 调用Lottery::draw
  9. pls->GraphicalObject::draw();       // 调用GraphicalObject::draw
复制代码

注意,在这种情况下,即使pls指向的是SpecialLotterySimulation对象,也无法(没有 "向下转换" ---- 参见技巧39)调用这个类中定义的draw函数。

没完,还有呢。Lottery和GraphicalObject中的draw函数都被声明为虚函数,所以子类可以重新定义它们(见技巧36),但如果LotterySimulation想对二者都重新定义那该怎么办?令人沮丧的是,这不可能,因为一个类只允许有唯一一个没有参数、名称为draw的函数。(这个规则有个例外,即一个函数为const而另一个不是的时候 ---- 见技巧21)

从某一方面来说,这个问题很严重,严重到足以成为修改C++语言的理由。ARM中就讨论了一种可能,即,允许被继承的虚函数可以 "改名" ;但后来又发现,可以通过增加一对新类来巧妙地避开这个问题:

  1. class AuxLottery: public Lottery {
  2. public:
  3.   virtual int lotteryDraw() = 0;
  4.   virtual int draw() { return lotteryDraw(); }
  5. };
  6. class AuxGraphicalObject: public GraphicalObject {
  7. public:
  8.   virtual int graphicalObjectDraw() = 0;
  9.   virtual int draw() { return graphicalObjectDraw(); }
  10. };
  11. class LotterySimulation: public AuxLottery,
  12.                          public AuxGraphicalObject {
  13. public:
  14.   virtual int lotteryDraw();
  15.   virtual int graphicalObjectDraw();
  16.   ...
  17. };
复制代码

这两个新类, AuxLottery和AuxGraphicalObject,本质上为各自继承的draw函数声明了新的名字。新名字以纯虚函数的形式提供,本例中即lotteryDraw和graphicalObjectDraw;函数是纯虚拟的,所以具体的子类必须重新定义它们。另外,每个类都重新定义了继承而来的draw函数,让它们调用新的纯虚函数。最终效果是,在这个类体系结构中,有二义的单个名字draw被有效地分成了无二义但功能等价的两个名字:lotteryDraw和graphicalObjectDraw:

  1. LotterySimulation *pls = new LotterySimulation;
  2. Lottery *pl = pls;
  3. GraphicalObject *pgo = pls;
  4. // 调用LotterySimulation::lotteryDraw
  5. pl->draw();
  6. // 调用LotterySimulation::graphicalObjectDraw
  7. pgo->draw();
复制代码

这是一个集纯虚函数,简单虚函数和内联函数(参见技巧33)综合应用之大成的方法,值得牢记在心。首先,它解决了问题,这个问题说不定哪天你就会碰到。其次,它可以提醒你,使用多继承会导致复杂性。是的,这个方法解决了问题,但仅仅为了重新定义一个虚函数而不得不去引入新的类,你真的愿意这样做吗?AuxLottery和AuxGraphicalObject类对于保证类层次结构的正确运转是必需的,但它们既不对应于问题范畴(problem domain )的某个抽象,也不对应于实现范畴(implementation domain)的某个抽象。它们单纯是作为一种实现设备而存在,再没有别的用处。你一定知道,好的软件是 "设备无关" 的,这条法则在此也适用。
将来使用MI还会面临更多的问题,二义性问题(尽管有趣)只不过是刚开始。另一个问题基于这样一个实践经验:一个起初象下面这样的继承层次结构:

  1. class B { ... };
  2. class C { ... };
  3. class D: public B, public C { ... };
复制代码

B    C
                     /    /
                      /  /
                       //
                       D
往往最后悲惨地发展成象下面这样:

  1. class A { ... };
  2. class B : virtual public A { ... };
  3. class C : virtual public A { ... };
  4. class D: public B, public C { ... };
复制代码

A  
                       //
                      /  /
                     /    /
                    B    C
                     /    /
                      /  /
                       //
                       D
钻石可能是女孩最好的朋友,也许不是;但肯定的是,象这样一种钻石形状的继承结构绝对不可能成为我们的朋友。如果创建了象这样的层次结构,就会立即面临这样一个问题:是不是该让A成为虚基类呢?即,从A的继承是否应该是虚拟的呢?现实中,答案几乎总是 ---- 应该;只有极少数情况下会想让类型D的对象包含A的数据成员的多个拷贝。正是认识到这一事实,上面的B和C将A声明为虚基类。

遗憾的是,在定义B和C的时候,你可能不知道将来是否会有类去同时继承它们,而且知不知道这一点实际上对正确地定义这两个类没有必要。对类的设计者来说,这实在是进退两难。如果不将A声明为B和C的虚基类,今后D的设计者就有可能需要修改B和C的定义,以便更有效地使用它们。通常,这很难做到,因为A,B和C的定义往往是只读的。例如这样的情况:A,B和C在一个库中,而D由库的用户来写。

另一方面,如果真的将A声明为B和C的虚基类,往往会在空间和时间上强加给用户额外的开销。因为虚基类常常是通过对象指针来实现的,并非对象本身。自不必说,内存中对象的分布是和编译器相关的,但一条不变的事实是:如果A作为 "非虚" 基类,类型D的对象在内存中的分布通常占用连续的内存单元;如果A作为 "虚" 基类,有时,类型D的对象在内存中的分布占用连续的内存单元,但其中两个单元包含的是指针,指向包含虚基类数据成员的内存单元:

A是非虚基类时D对象通常的内存分布:
           A部分+ B部分+ A部分 + C部分 + D部分

A是虚基类时D对象在某些编译器下的内存分布:
                           ------------------------------------------------
                           |                                            |
                           |                                            +
          B部分 + 指针 + C部分 + 指针 + D部分 + A部分
                                                      |                 +
                                                      |                 |
                                                      ------------------------

即使编译器不采用这种特殊的实现策略,使用虚继承通常也会带来某种空间上的惩罚。

考虑到这些因素,看来,在进行高效的类设计时如果涉及到MI,作为库的设计者就要具有超凡的远见。然而现在的年代,常识都日益成为了稀有品,因而你会不明智地过多依赖于语言特性,这就不仅要求设计者能够预计得到未来的需要,而且简直就是要你做到彻底的先知先觉(参见技巧M32)。

当然,这也可以说成是在虚函数和非虚函数间选择,但还是有重大的不同。技巧36说明,虚函数具有定义明确的高级含义,非虚函数也同样具有定义明确的高级含义,而且它们的含义有显著的不同,所以在清楚自己想对子类的设计者传达什么含义的基础上,在二者之间作出选择是可能的。但是,决定基类是否应该是虚拟的,则缺乏定义明确的高级含义;相反,决定通常取决于整个继承的层次结构,所以除非知道了整个层次结构,否则无法做出决定。如果正确地定义出个类之前需要清楚地知道将来怎么使用它,这种情况下将很难设计出高效的类。

就算避开了二义性问题,并且解决了是否应该从基类虚拟继承的疑问,还是会有许多复杂性问题等着你。为了长话短说,在此我仅提出应该记住的其它两点:

? 向虚基类传递构造函数参数。非虚继承时,基类构造函数的参数是由紧临的派生类的成员初始化列表指定的。因为单继承的层次结构只需要非虚基类,继承层次结构中参数的向上传递采用的是一种很自然的方式:第n层的类将参数传给第n-1层的类。但是,虚基类的构造函数则不同,它的参数是由继承结构中最底层派生类的成员初始化列表指定的。这就造成,负责初始化虚基类的那个类可能在继承图中和它相距很远;如果有新类增加到继承结构中,执行初始化的类还可能改变。(避免这个问题的一个好办法是:消除对虚基类传递构造函数参数的需要。最简单的做法是避免在这样的类中放入数据成员。这本质上是Java的解决之道:Java中的虚基类(即,"接口")禁止包含数据)

? 虚函数的优先度。就在你自认为弄清了所有的二义之时,它们却又在你面前摇身一变。再次看看关于类A,B,C和D的钻石形状的继承图。假设A定义了一个虚成员函数mf,C重定义了它;B和D则没有重定义mf:

             A   virtual void mf();
             //
            /  /
           /    /
          B    C   virtual void mf();
           /    /
            /  /
             //
             D

根据以前的讨论,你会认为下面有二义:

  1. D *pd = new D;
  2. pd->mf();                      // A::mf或者C::mf
复制代码

该为D的对象调用哪个mf呢,是直接从C继承的还是间接(通过B)从A继承的那个呢?答案取决于B和C如何从A继承。具体来说,如果A是B或C的非虚基类,调用具有二义性;但如果A是B和C的虚基类,就可以说C中mf的重定义优先度高于最初A中的定义,因而通过pd对mf的调用将(无二义地)解析为C::mf。如果你坐下来仔细想想,这正是你想要的行为;但需要坐下仔细想想才能弄懂,也确实是一种痛苦。

也许至此你会承认MI确实会导致复杂化。也许你认识到每个人其实都不想使用它。也许你准备建议国际C++标准委员会将多继承从语言中去掉;或者至少你想向你的老板建议,全公司的程序员都禁止使用它。

也许你太性急了。

请记住,C++的设计者并没有想让多继承难以使用;恰恰是,想让一切都能以更合理的方式协调工作,这本身会带来某些复杂性。上面的讨论中你会注意到,这些复杂性很多是由于使用虚基类引起的。如果能避免使用虚基类 ---- 即,如果能避免产生那种致命的钻石形状继承图 ---- 事情就好处理多了。

例如,技巧34中讲到,协议类(Protocol class)的存在仅仅是为派生类制定接口;它没有数据成员,没有构造函数,有一个虚析构函数(参见技巧14),有一组用来指定接口的纯虚函数。一个Person协议类看起来象下面这样:

  1. class Person {
  2. public:
  3.   virtual ~Person();
  4.   virtual string name() const = 0;
  5.   virtual string birthDate() const = 0;
  6.   virtual string address() const = 0;
  7.   virtual string nationality() const = 0;
  8. };
复制代码

这个类的用户在编程时必须使用Person的指针或引用,因为抽象类不能被实例化。

为了创建 "可以作为Person对象而使用" 的对象,Person的用户使用工厂函数(factory function,参见技巧34)来实例化具体的子类:

  1. // 工厂函数,从一个唯一的数据库ID
  2. // 创建一个Person对象
  3. Person * makePerson(DatabaseID personIdentifier);
  4. DatabaseID askUserForDatabaseID();
  5. DatabaseID pid = askUserForDatabaseID();
  6. Person *pp = makePerson(pid);    // 创建支持Person
  7.                                  // 接口的对象
  8. ...                              // 通过Person的成员函数
  9.                                  // 操作*pp
  10. delete pp;                       // 删除不再需要的对象
复制代码

这就带来一个问题:makePerson返回的指针所指向的对象如何创建呢?显然,必须从Person派生出某种具体类,使得makePerson可以对其进行实例化。

假设这个类被称为MyPerson。作为一个具体类,MyPerson必须实现从Person继承而来的纯虚函数。这可以从零做起,但如果已经存在一些组件可以完成大多数或全部所需的工作,那么从软件工程的角度来说,能利用这些组件将再好不过。例如,假设已经有一个和数据库有关的旧类PersonInfo,它提供的功能正是MyPerson所需要的:

  1. class PersonInfo {
  2. public:
  3.   PersonInfo(DatabaseID pid);
  4.   virtual ~PersonInfo();
  5.   virtual const char * theName() const;
  6.   virtual const char * theBirthDate() const;
  7.   virtual const char * theAddress() const;
  8.   virtual const char * theNationality() const;
  9.   virtual const char * valueDelimOpen() const;       // 看下文
  10.   virtual const char * valueDelimClose() const;   
  11.   ...
  12. };
复制代码

可以断定这是一个很旧的类,因为它的成员函数返回的是const char*而不是string对象。但是,如果鞋合脚,为什么不穿呢?这个类的成员函数名暗示,这双鞋穿上去会很舒服。

随之你会发现,当初设计PersonInfo是用来方便地以各种不同格式打印数据库字段,每个字段值的开头和结尾用特殊字符串分开。默认情况下,字段值的起始分隔符和结束分隔符为括号,所以字段值 "Ring-tailed Lemur" 将会这样被格式化:

[Ring-tailed Lemur]

因为括号不是所有PersonInfo的用户都想要的,虚函数valueDelimOpen和valueDelimClose允许派生类指定它们自己的起始分隔符和结束分隔符。PersonInfo类的theName,theBirthDate,theAddress以及theNationality的实现将调用这两个虚函数,在它们的返回值中添加适当的分隔符。拿PersonInfo::name作为例子,代码看起来象这样:

  1. const char * PersonInfo::valueDelimOpen() const
  2. {
  3.   return "[";                   // 默认起始分隔符
  4. }
  5. const char * PersonInfo::valueDelimClose() const
  6. {
  7.   return "]";                   // 默认结束分隔符
  8. }
  9. const char * PersonInfo::theName() const
  10. {
  11.   // 为返回值保留缓冲区。因为是静态
  12.   // 类型,它被自动初始化为全零。
  13.   static char value[MAX_FORMATTED_FIELD_VALUE_LENGTH];
  14.   // 写起始分隔符
  15.   strcpy(value, valueDelimOpen());
  16.   将对象的名字字段值添加到字符串中
  17.   // 写结束分隔符
  18.   strcat(value, valueDelimClose());
  19.   return value;
  20. }
复制代码

有些人会挑剔PersonInfo::theName的设计(特别是使用了固定大小的静态缓冲区 ---- 参见技巧23),但请将你的挑剔放在一边,关注这一点:首先,theName调用valueDelimOpen,生成它将要返回的字符串的起始分隔符;然后,生成名字值本身;最后,调用valueDelimClose。因为valueDelimOpen和valueDelimClose是虚函数,theName返回的结果既依赖于PersonInfo,也依赖于从PersonInfo派生的类。

作为MyPerson的实现者,这是条好消息,因为在研读Person文档的细则时你发现,name及其相关函数需要返回的是不带修饰的值,即,不允许带分隔符。也就是说,如果一个人来自Madagascar,调用这个人的nationality函数将返回"Madagascar",而不是 "[Madagascar]"。

MyPerson和PersonInfo之间的关系是,PersonInfo刚好有些函数使得MyPerson易于实现。仅次而已。没看到有 "是一个" 或 "有一个" 的关系。它们的关系是 "用...来实现",而且我们知道,这可以用两种方式来表示:通过分层(见技巧40)和通过私有继承(见技巧42)。技巧42指出,分层一般来说是更好的方法,但在有虚函数要被重新定义的情况下,需要使用私有继承。现在的情况是,MyPerson需要重新定义valueDelimOpen和valueDelimClose,所以不能用分层,而必须用私有继承:MyPerson必须从PersonInfo私有继承。

但MyPerson还必须实现Person接口,因而需要公有继承。这导致了多继承一个很合理的应用:将接口的公有继承和实现的私有继承结合起来:

  1. class Person {                        // 这个类指定了
  2. public:                               // 需要被实现
  3.   virtual ~Person();                  // 的接口
  4.   virtual string name() const = 0;
  5.   virtual string birthDate() const = 0;
  6.   virtual string address() const = 0;
  7.   virtual string nationality() const = 0;
  8. };
  9. class DatabaseID { ... };             // 被后面的代码使用;
  10.                                       // 细节不重要
  11. class PersonInfo {                    // 这个类有些有用
  12. public:                               // 的函数,可以用来
  13.   PersonInfo(DatabaseID pid);         // 实现Person接口
  14.   virtual ~PersonInfo();
  15.   virtual const char * theName() const;
  16.   virtual const char * theBirthDate() const;
  17.   virtual const char * theAddress() const;
  18.   virtual const char * theNationality() const;
  19.   virtual const char * valueDelimOpen() const;
  20.   virtual const char * valueDelimClose() const;
  21.   ...
  22. };
  23. class MyPerson: public Person,        // 注意,使用了
  24.                 private PersonInfo {  // 多继承
  25. public:
  26.   MyPerson(DatabaseID pid): PersonInfo(pid) {}
  27.   // 继承来的虚分隔符函数的重新定义
  28.   const char * valueDelimOpen() const { return ""; }
  29.   const char * valueDelimClose() const { return ""; }
  30.   // 所需的Person成员函数的实现
  31.   string name() const
  32.   { return PersonInfo::theName(); }
  33.   string birthDate() const
  34.   { return PersonInfo::theBirthDate(); }
  35.   string address() const
  36.   { return PersonInfo::theAddress(); }
  37.   string nationality() const
  38.   { return PersonInfo::theNationality(); }
  39. };
复制代码

用图形表示,看起来象下面这样:

         Person       PersonInfo
                     /    /
                      /  /
                       //
                   MyPerson

这种例子证明,MI会既有用又易于理解,尽管可怕的钻石形状继承图不会明显消失。

然而,必须当心诱惑。有时你会掉进这样的陷阱中:对某个需要改动的继承层次结构来说,本来用一个更基本的重新设计可以更好,但你却为了追求速度而去使用MI。例如,假设为可以活动的卡通角色设计一个类层次结构。至少从概念上来说,让各种角色能跳舞唱歌将很有意义,但每一种角色执行这些动作时方式都不一样。另外,跳舞唱歌的缺省行为是什么也不做。

所有这些用C++来表示就象这样:

  1. class CartoonCharacter {
  2. public:
  3.   virtual void dance() {}
  4.   virtual void sing() {}
  5. };
复制代码

虚函数自然地体现了这样的约束:唱歌跳舞对所有CartoonCharacter对象都有意义。什么也不做的缺省行为通过类中那些函数的空定义来表示(参见技巧36)。假设有一个特殊类型的卡通角色是蚱蜢,它以自己特殊的方式跳舞唱歌:

  1. class Grasshopper: public CartoonCharacter {
  2. public:
  3.   virtual void dance();    // 定义在别的什么地方
  4.   virtual void sing();     // 定义在别的什么地方
  5. };
复制代码

现在假设,在实现了Grasshopper类后,你又想为蟋蟀增加一个类:

  1. class Cricket: public CartoonCharacter {
  2. public:
  3.   virtual void dance();
  4.   virtual void sing();
  5. };
复制代码

当坐下来实现Cricket类时,你意识到,为Grasshopper类所写的很多代码可以重复使用。但这需要费点神,因为要到各处去找出蚱蜢和蟋蟀唱歌跳舞的不同之处。你猛然间想出了一个代码复用的好办法:你准备用Grasshopper类来实现Cricket类,你还准备使用虚函数以使Cricket类可以定制Grasshopper的行为。

你立即认识到这两个要求 ---- "用...来实现" 的关系,以及重新定义虚函数的能力 ---- 意味着Cricket必须从Grasshopper私有继承,但蟋蟀当然还是一个卡通角色,所以你通过同时从Grasshopper和CartoonCharacter继承来重新定义Cricket:

  1. class Cricket: public CartoonCharacter,
  2.                private Grasshopper {
  3. public:
  4.   virtual void dance();
  5.   virtual void sing();
  6. };
复制代码

然后准备对Grasshopper类做必要的修改。特别是,需要声明一些新的虚函数让Cricket重新定义:

  1. class Grasshopper: public CartoonCharacter {
  2. public:
  3.   virtual void dance();
  4.   virtual void sing();
  5. protected:
  6.   virtual void danceCustomization1();
  7.   virtual void danceCustomization2();
  8.   virtual void singCustomization();
  9. };
复制代码

蚱蜢跳舞现在被定义成象这样:

  1. void Grasshopper::dance()
  2. {
  3.   执行共同的跳舞动作;
  4.   danceCustomization1();
  5.   执行更多共同的跳舞动作;
  6.   danceCustomization2();
  7.   执行最后共同的跳舞动作;
  8. }
复制代码

蚱蜢唱歌的设计与此类似。
很明显,Cricket类必须修改一下,因为它必须重新定义新的虚函数:

  1. class Cricket:public CartoonCharacter,
  2.       private Grasshopper {
  3. public:
  4.   virtual void dance() { Grasshopper::dance(); }
  5.   virtual void sing() { Grasshopper::sing(); }
  6. protected:
  7.   virtual void danceCustomization1();
  8.   virtual void danceCustomization2();
  9.   virtual void singCustomization();
  10. };
复制代码

这看来很不错。当需要Cricket对象去跳舞时,它执行Grasshopper类中共同的dance代码,然后执行Cricket类中定制的dance代码,接着继续执行Grasshopper::dance中的代码,等等。

然而,这个设计中有个严重的缺陷,这就是,你不小心撞上了 "奥卡姆剃刀" ---- 任何一种奥卡姆剃刀都是有害的思想,William of Occam的尤其如此。奥卡姆者鼓吹:如果没有必要,就不要增加实体。现在的情况下,实体就是指的继承关系。如果你相信多继承比单继承更复杂的话(我希望你相信),Cricket类的设计就没必要复杂。(译注:1) William of Occam(1285-1349),英国神学家,哲学家。2) 奥卡姆剃刀(Occam's razor)是一种思想,主要由William of Occam提出。之所以将它称为 "奥卡姆剃刀",是因为William of Occam经常性地、很锐利地运用这一思想。)

问题的根本之处在于,Cricket类和Grasshopper类之间并非 "用...来实现" 的关系。而是,Cricket类和Grasshopper类之间享有共同的代码。特别是,它们享有决定唱歌跳舞行为的代码 ---- 蚱蜢和蟋蟀都有这种共同的行为。

说两个类具有共同点的方式不是让一个类从另一个类继承,而是让它们都从一个共同的基类继承,蚱蜢和蟋蟀之间的公共代码不属于Grasshopper类,也不属于Cricket,而是属于它们共同的新的基类,如,Insect:

  1. class CartoonCharacter { ... };
  2. class Insect: public CartoonCharacter {
  3. public:
  4.   virtual void dance();    // 蚱蜢和蟋蟀
  5.   virtual void sing();     // 的公共代码
  6. protected:
  7.   virtual void danceCustomization1() = 0;
  8.   virtual void danceCustomization2() = 0;
  9.   virtual void singCustomization() = 0;
  10. };
  11. class Grasshopper: public Insect {
  12. protected:
  13.   virtual void danceCustomization1();
  14.   virtual void danceCustomization2();
  15.   virtual void singCustomization();
  16. };
  17. class Cricket: public Insect {
  18. protected:
  19.   virtual void danceCustomization1();
  20.   virtual void danceCustomization2();
  21.   virtual void singCustomization();
  22. };
复制代码

CartoonCharacter
                        |
                        |
                   Insect
                       //
                      /  /
                     /    /
Grasshopper     Cricket

可以看到,这个设计更清晰。只是涉及到单继承,此外,只是用到了公有继承。Grasshopper和Cricket定义的只是定制功能;它们从Insect一点没变地继承了dance和sing函数。William of Occam一定会很骄傲。

尽管这个设计比采用了MI的那个方案更清晰,但初看可能会觉得比使用MI的还要逊色。毕竟,和MI的方案相比,这个单继承结构中引入了一个全新的类,而使用MI就不需要。如果没必要,为什么要引入一个额外的类呢?

这就将你带到了多继承诱人的本性面前。表面看来,MI好象使用起来更容易。它不需要增加新的类,虽然它要求在Grasshopper类中增加一些新的虚函数,但这些函数在任何情况下都是要增加的。

设想有个程序员正在维护一个大型C++类库,现在需要在库中增加一个新的类,就象Cricket类要被增加到现有的的CartoonCharacter/Grasshopper层次结构中一样。程序员知道,有大量的用户使用现有的层次结构,所以,库的变化越大,对用户的影响越大。程序员决心将这种影响降低到最小。对各种选择再三考虑之后,程序员认识到,如果增加一个从Grasshopper到Cricket的私有继承连接,层次结构中将不需要任何其它变化。程序员不禁因为这个想法露出了微笑,暗自庆幸今后可以大量地增加功能,而代价仅仅只是增加很小一点复杂性。

现在设想这个负责维护的程序员是你。那么,请抵御这一诱惑!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值