C++程序开发中,设计孤立的类比较容易,设计相互关联的类却比较难,这其中会涉及到两个概念,一个是继承(Inheritance),一个是组合(Composition)。因为二者有一定的相似性,往往令程序员混淆不清。类的组合和继承一样,是软件重用的重要方式。组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。

如果类B 有必要使用A 的功能,则要分两种情况考虑:

1.继承

若在逻辑上B 是一种A (is a kind of),则允许B 继承A 的功能,它们之间就是Is-A 关系。如男人(Man)是人(Human)的一种,女人(Woman)是人的一种。那么类Man 可以从类Human 派生,类Woman也可以从类Human 派生。示例程序如下:

class Human
{
  …
};
class Man : public Human
{
  …
};
class Woman : public Man
{
  …
};

在UML的术语中,继承关系被称为泛化(Generalization),类Man和Woman与类Human的UML关系图可描述如下:

继承在逻辑上看起来比较简单,但在实际应用上可能遭遇意外。比如在OO界中著名的“鸵鸟不是鸟”和“圆不是椭圆”的问题。这样的问题说明了程序设计和现实世界存在逻辑差异。从生物学的角度,鸵鸟(Ostrich)是鸟(Bird)的一种,既然是Is-A的关系,类COstrich应该可以从类CBird派生。但是鸵鸟不会飞,但从CBird那里继承了接口函数fly,如下所示:

class CBird{
public:
    virtual void fly(){}
};

class COstrich{
public:
    ...
};

“圆不是椭圆”同样存在类似的问题,圆从椭圆类继承了无用的长短轴数据成员。所以更加严格的继承应该是:若在逻辑上B是A的一种,并且A的所有功能和属性对B都有意义,则允许B继承A的所有功能和属性。

类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的,所以我们一般称之为白盒复用。继承易于修改或扩展那些被复用的实现,但它这种白盒复用却容易破坏封装性。因为这会将父类的实现细节暴露给子类。

2.组合

若在逻辑上A 是B 的“一部分”(a part of),则不允许B 继承A 的功能,而是要用A和其它东西组合出B,它们之间就是“Has-A关系”。例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head 应该由类Eye、Nose、Mouth、Ear 组合而成,不是派生而成。示例程序如下:

class Eye
{
public:
  void Look(void);
};
class Nose
{
public:
  void Smell(void);
};
class Mouth
{
public:
  void Eat(void);
};
class Ear
{
public:
  void Listen(void);
};
// 正确的设计,冗长的程序
class Head
{
public:
  void Look(void) { m_eye.Look(); }
  void Smell(void) { m_nose.Smell(); }
  void Eat(void) { m_mouth.Eat(); }
  void Listen(void) { m_ear.Listen(); }
private:
  Eye m_eye;
  Nose m_nose;
  Mouth m_mouth;
Ear m_ear;
};

如果允许Head 从Eye、Nose、Mouth、Ear 派生而成,那么Head 将自动具有Look、Smell、Eat、Listen 这些功能:

// 错误的设计
class Head : public Eye, public Nose, public Mouth, public Ear
{
};

上述程序十分简短并且运行正确,但是这种设计却是错误的。所以我们要经的起“继承”的诱惑,避免犯下设计错误。

在UML中,上面类的UML关系图可描述如下:



实心菱形代表了一种坚固的关系,被包含类的生命周期受包含类控制,被包含类会随着包含类创建而创建,消亡而消亡。组合属于黑盒复用,被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合。而缺点就是致使系统中的对象过多。

综上所述,Is-A关系用继承表示,Has-A关系用组合表示,GoF在《设计模式》中指出OO设计的一大原则就是:优先使用对象组合,而不是类继承。

3.解决“圆不是椭圆”继承问题,杜绝不良继承

封装、继承、多态是面向对象技术的三大机制,封装是基础、继承是关键、多态是延伸。继承是作为关键的一部分,如果我们理解不够深刻,则容易造成程序设计中的不良继承,影响程序质量。

上文中“圆不是椭圆”这一著名问题,实际上在数学上圆是一种特殊的椭圆,于是会出现下面的继承:

class CEllipse{
public:
    void setSize(float x,float y){}
};

class CCircle:public CEllipse{};

椭圆存在一个设置长短轴的成员函数setSize,而圆则不需要。椭圆能做某些圆不能做的事,所以圆继承自椭圆是不合理的类设计。那么面对“圆是/不是一种椭圆”这个两难的问题,我们如何解决。主要有几下几种方法:

(1)使用代码技巧来弥补设计缺陷。在子类CCircle中重新定义setSize抛出异常,或终止程序,或做其他的异常处理,但这些技巧会让用户吃惊不已,违背了接口设计的“最小惊讶原则”;

(2)改变观点,人为圆是不对称的。这对于我们思维严谨的程序员来说,有点不可接受;

(3)将基类的成员函数setSize删除。但这回影响椭圆对象的正常使用。

(4)去掉它们之间的继承关系。推荐做法,既然圆继承椭圆是一种不良类设计,我们就应该杜绝。去掉继承关系,并不代表圆与椭圆就没有关系,两个类可以继承自同一个类COvalShape,不过该类不能执行不对称的setSize计算,如下图所示:

class COvalShape{
public:
    void setSize(float x);
};

class  CEllipse:public COvalShape{
public:
    void setSize(float x,float y);
};

class CCircle:public COvalShape{
};

其中,椭圆增加了特有的setSize(float x,float y)运算。

不良继承出现的根本原因在于对继承的理解不够深刻,错把直觉中的“是一种(Is-A)”当成了学术中的“子类型(subtype)”概念。在继承体系中,派生类对象是可以取代基类对象的。而在椭圆和圆的问题上,椭圆类中的成员函数setSize(x,y)违背了这个可置换性,即Liskov替换原则。

所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。


参考文献

[1]C++中继承和组合区别与使用

[2]李健.编写高质量代码:改善C++程序的150个建议.第一版.北京:机械工业出版社,2012.1:303-310

C++继承与组合的区别的更多相关文章

  1. java中继承和组合的区别

    子类继承父类,父类的所有属性和方法都可以被子类访问和调用.组合是指将已存在的类型作为一个新建类的成员变量类型,又叫"对象持有". 通过组合和继承,都可以实现系统功能的重用和代码的复 ...

  2. c++ 继承和组合的区别

    .什么是继承 A继承B,说明A是B的一种,并且B的所有行为对A都有意义 eg:A=WOMAN B=HUMAN A=鸵鸟 B=鸟 (不行),因为鸟会飞,但是鸵鸟不会. .什么是组合 若在逻辑上A是B的“ ...

  3. Objective-C的继承与组合

    Objective-C的继承与组合 Objective-C与Java继承上的区别 区别 Objective-C Java 成员变量 Objective-C继承不允许子类和父类拥有相同名称的成员变量 J ...

  4. <Java中的继承和组合之间的联系和区别>

    //Java中的继承和组合之间的联系和区别 //本例是继承 class Animal { private void beat() { System.out.println("心胀跳动...& ...

  5. Inheritance, Association, Aggregation, and Composition 类的继承,关联,聚合和组合的区别

    在C++中,类与类之间的关系大概有四种,分别为继承,关联,聚合,和组合.其中继承我们大家应该都比较熟悉,因为是C++的三大特性继承Inheritance,封装Encapsulation,和多态Poly ...

  6. Java中的继承与组合(转载)

    本文主要说明Java中继承与组合的概念,以及它们之间的联系与区别.首先文章会给出一小段代码示例,用于展示到底什么是继承.然后演示如何通过“组合”来改进这种继承的设计机制.最后总结这两者的应用场景,即到 ...

  7. Java中的继承与组合

    本文主要说明Java中继承与组合的概念,以及它们之间的联系与区别.首先文章会给出一小段代码示例,用于展示到底什么是继承.然后演示如何通过“组合”来改进这种继承的设计机制.最后总结这两者的应用场景,即到 ...

  8. Go如何使用实现继承的组合

    Go它提供了一个非常值得称道的并发支持,但Go它不支持完全面向对象的.这并不意味着Go不支持面向对象,,和Go的OO系统做的很轻巧,学习降至最低成本.向对象让Go失去了一些OO的方便特性,可是更高的效 ...

  9. 三张图搞懂JavaScript的原型对象与原型链 / js继承,各种继承的优缺点(原型链继承,组合继承,寄生组合继承)

    摘自:https://www.cnblogs.com/shuiyi/p/5305435.html 对于新人来说,JavaScript的原型是一个很让人头疼的事情,一来prototype容易与__pro ...

随机推荐

  1. 3星|《实战复盘第四季·商业巨头们的变革之道》:GE、TCL、力拓集团、英美资源集团等企业总裁的变更经验

    实战复盘第四季·商业巨头们的变革之道(<哈佛商业评论>增刊) 本期是<哈佛商业评论>“实战复盘”栏目的10篇文章,讲的是GE.TCL.力拓集团.英美资源集团等企业如何熬过变革期 ...

  2. 虚拟机中安装MAC OS X教程(适用所有电脑方法,特别是cpu不支持硬件虚拟化的电脑)

    前言 之前写了一篇在Windows上搭建Object-C开发环境,并且写了一个HelloWorld程序.但真正开发苹果软件是在MAC OS X系统中(以下简称OSX)中.买不起MacBook,也没有O ...

  3. 主成分分析——PCA

    在数据挖掘过程中,当一个对象有多个属性(即该对象的测量过程产生多个变量)时,会产生高维度数据,这给数据挖掘工作带来了难度,我们希望用较少的变量来描述数据的绝大多数信息,此时一个比较好的方法是先对数据进 ...

  4. 北美跨境电商平台Wish透露未来一年在华规划

    9月12日,北美跨境电商平台Wish在深圳透露了未来一年在中国区的重点规划.Wish中国区总裁丁浩川表示,在下一阶段,Wish公司将继续围绕 提升平台流量. 加强品类支撑. 深化库存管理. 推进物流改 ...

  5. Notes of Daily Scrum Meeting(11.10)

    Notes of Daily Scrum Meeting(11.10) 今天是周一,虽然仍然在假期里,但是我们仍然要继续我们团队的开发工作了,分工大家已然都很明确,所以接下来 就是认真投入,把自己负责 ...

  6. 实验1 熟悉Linux开发环境 实验报告

    参见http://www.cnblogs.com/lhc-java/p/4970269.html

  7. Java未赋值变量的默认初始值

    在 Java 程序中,任何变量都必须经初始化后才能被使用.当一个对象被创建时,实例变量在分配内存空间时按程序员指定的初始化值赋值,否则系统将按下列默认值进行初始化: 数据类型 初始值 byte 0 s ...

  8. Leetcode题库——11.盛最多水的容器

    @author: ZZQ @software: PyCharm @file: maxArea.py @time: 2018/10/11 21:47 说明:给定 n 个非负整数 a1,a2,...,an ...

  9. The user survey(用户调查)

    在周末,我们找了一些人来进行了一个调查,鉴于选择困难,我们只找到了几个真正的小学生,没有找到家长,其余那些都是找大学生来做调查的,我们和他们说,让他们把自己的立场看成是小学生或家长.下面是我们整理出来 ...

  10. 学习率(Learning rate)的理解以及如何调整学习率

    1. 什么是学习率(Learning rate)?   学习率(Learning rate)作为监督学习以及深度学习中重要的超参,其决定着目标函数能否收敛到局部最小值以及何时收敛到最小值.合适的学习率 ...