比继承更偏爱组成?


1603

为什么更偏重于继承而不是继承?每种方法都有哪些取舍?什么时候应该选择继承而不是合成?



40
有一个关于这个问题的好文章在这里。我个人认为设计没有“更好”或“更差”的原则。有针对具体任务的“适当的”和“不足的”设计。换句话说-根据情况,我同时使用继承或组合。目标是产生更小的代码,更易于阅读,重用并最终进一步扩展。
m_pGladiator

1
一句话,如果您拥有公共方法,则继承是公共的,并且您对其进行了更改,它将更改已发布的api。如果您具有组成并且组成的对象已更改,则不必更改已发布的api。
Tomer Ben David

Answers:


1188

优先考虑组成而不是继承,因为它在以后更易于延展/更容易修改,但不要使用总是组合的方法。通过组合,可以轻松地通过“依赖注入/设置器”即时更改行为。继承更加严格,因为大多数语言不允许您从一种以上的类型派生。因此,一旦从TypeA派生,鹅或多或少就会被煮熟。

我对上面的酸测试是:

  • TypeB是否要公开TypeA的完整接口(所有公共方法不少于此),以便可以在需要TypeA的地方使用TypeB?表示继承

    • 例如,塞斯纳(Cessna)双翼飞机将展示飞机的完整界面,甚至更多。因此,它很适合从飞机中派生。
  • TypeB是否只希望TypeA公开某些/部分行为?表示需要组成。

    • 例如,鸟类可能只需要飞机的飞行行为。在这种情况下,将其提取为接口/类/两者并使其成为这两个类的成员是有意义的。

更新:刚刚回到我的答案,现在似乎没有完整提及Barbara Liskov的Liskov替代原则,这是不完整的,以检验“我是否应该继承这种类型?”


81
第二个示例直接出自《 Head First设计模式》(amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/…)本书:)我强烈建议将该书推荐给使用此问题的任何人。
Jeshurun

4
这很清楚,但是可能会遗漏一些东西:“ TypeB是否要公开TypeA的完整接口(不少于所有公共方法),以便可以在需要TypeA的地方使用TypeB?” 但是,如果这是真的,并且TypeB也公开TypeC的完整接口怎么办?如果尚未建模TypeC呢?
特里斯坦

4
您提到我认为应该是最基本的测试:“该对象是否可以被期望基本类型的对象使用的代码所使用”。如果答案是肯定的,则该对象必须继承。如果否,则可能不应该。如果我有德鲁特,语言将提供一个关键字来引用“这个类”,并提供一种定义一个类的方法,该类的行为应与另一个类相同,但不能代替它(这样的类将具有所有“ this”类”引用替换为自身)。
超级猫

22
@Alexey-关键是“我可以将Cessna双翼飞机传递给所有希望乘坐飞机的客户而不会感到惊讶吗?”。如果是,那么您很可能希望继承。
Gishu 2012年

9
实际上,我很难考虑任何可以继承的示例,我经常发现聚合,组合和接口可以提供更优雅的解决方案。使用这些方法可能可以更好地解释上述许多示例……
Stuart Wakefield

413

将遏制视为关系。汽车“具有”发动机,人“具有”名称,等等。

想继承的作为是一个关系。汽车“是”车辆,人“是”哺乳动物,等等。

我不相信这种方法。我把它直接从代码的第二版完全史蒂夫·麦康奈尔第6.3节


107
这并不总是一个完美的方法,它只是一个很好的指南。Liskov替代原理更准确(失败更少)。
比尔K

40
“我的车有车。” 如果您将其视为一个单独的句子,而不是在编程环境中,那绝对没有任何意义。这就是这项技术的重点。如果听起来很尴尬,那可能是错误的。
Nick Zalutskiy 2011年

36
@Nick可以,但是“我的汽车有VehicleBehavior”更有意义(我想您的“ Vehicle”类可以命名为“ VehicleBehavior”)。因此,您不能基于“具有”与“具有”的比较来决定,您必须使用LSP,否则您将犯错
Tristan

35
而不是“是”想到“行为”。继承与继承行为有关,而不仅仅是语义。
ybakos 2012年

4
@ybakos“行为类似”可以通过接口实现,而无需继承。摘自Wikipedia“通过继承实现组合通常是从创建表示系统必须表现出的行为的各种接口开始的...因此,系统行为是在没有继承的情况下实现的。”
DavidRR

210

如果您了解其中的区别,则更容易解释。

程序代码

一个这样的例子是不使用类的PHP(特别是在PHP5之前)。所有逻辑都编码为一组功能。您可以包括其他包含辅助功能的文件,等等,并通过在功能中传递数据来进行业务逻辑。随着应用程序的增长,这可能很难管理。PHP5试图通过提供更多面向对象的设计来对此进行补救。

遗产

这鼓励使用类。继承是OO设计的三个宗旨之一(继承,多态,封装)。

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

这是工作中的继承。雇员“是”个人或从个人继承。所有继承关系都是“是”关系。Employee还会遮盖Person的Title属性,这意味着Employee.Title将返回Employee而不是Person的Title。

组成

合成胜于继承。简单地说,您将拥有:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

组成通常是“具有”或“使用”关系。这里的Employee类有一个Person。它不是从Person继承而来的,而是将Person对象传递给它的,这就是为什么它“具有” Person的原因。

继承而不是继承

现在说您要创建一个Manager类型,因此最终得到:

class Manager : Person, Employee {
   ...
}

此示例可以正常工作,但是,如果Person和Employee都声明了Title怎么办?Manager.Title应该返回“运营经理”还是“先生”?在组成下,这种歧义可以更好地解决:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Manager对象由Employee和Person组成。标题行为来自员工。这种明确的构成消除了其他方面的歧义,并且您将遇到较少的错误。


6
对于继承:没有歧义。您正在根据需求实现Manager类。因此,如果那是您的要求指定的内容,那么您将返回“运营经理”,否则您将只使用基类的实现。另外,您可以使Person为抽象类,从而确保下游类实现Title属性。
拉吉·饶

68
重要的是要记住,有人可能会说“组成在继承之上”,但这并不意味着“组成总是在继承之上”。“是”表示继承,并导致代码重用。员工是人(员工没有人)。
拉吉·饶

36
这个例子很令人困惑.Employee是一个人,因此应该使用继承。在本示例中,您不应该使用组合,因为即使在技术上您可以在代码中声明它,在领域模型中它也是错误的关系。
Michael Freidgeim

15
我不同意这个例子。雇员是一个人,这是正确使用继承的教科书。我还认为,对“标题”字段的重新定义没有意义。Employee.Title遮盖Person.Title的事实表明编程不佳。毕竟是“先生”。和“运营经理”真的指的是一个人的同一方面(小写)?我将重命名Employee.Title,从而能够引用Employee的Title和JobTitle属性,这在现实生活中都是有意义的。此外,没有理由担任经理(续...)
Radon Rosborough 2014年

9
(...继续)同时从Person和Employee继承-毕竟,Employee已经从Person继承。在一个人可能是经理和代理的更复杂的模型中,可以使用多个继承(小心!)是正确的,但是在许多环境中最好有一个抽象的角色类,经理(包含员工)他/她管理)和代理(包含合同及其他信息)继承。那么,雇员就是一个具有多个角色的人。因此,组合和继承都可以正确使用。
Radon Rosborough 2014年

141

继承带来了所有不可否认的好处,这是它的一些缺点。

继承的缺点:

  1. 您不能在运行时更改从超类继承的实现(显然是因为继承是在编译时定义的)。
  2. 继承向子类公开其父类实现的详细信息,这就是为什么人们常说继承会破坏封装(从某种意义上说,您实际上只需要专注于接口而不是实现,因此不总是首选通过子类进行重用)。
  3. 继承提供的紧密耦合使子类的实现与超类的实现非常紧密地绑定在一起,父级实现中的任何更改都将迫使子类进行更改。
  4. 子类的过度重用也会使继承堆栈非常深且非常混乱。

另一方面,对象组成是在运行时通过对象获取对其他对象的引用来定义的。在这种情况下,这些对象将永远无法访问彼此的受保护数据(没有封装中断),并且将被迫尊重彼此的接口。并且在这种情况下,实现依赖也将比继承情况少得多。


5
在我看来,这是更好的答案之一-根据我的经验,我试图在构图​​方面重新思考您的问题,这往往会导致更小,更简单,更独立,更可重用的类,职责范围更清晰,更小,更集中。通常,这意味着不需要诸如依赖注入或模拟(在测试中)之类的东西,因为较小的组件通常能够独立存在。只是我的经验。YMMV :-)
mindplay.dk 2014年

3
这篇文章的最后一段对我来说真的很有趣。谢谢。
Salx

87

另一个非常务实的原因是,首选组合而不是继承,这与您的域模型有关,并将其映射到关系数据库。很难将继承映射到SQL模型(您最终会遇到各种各样的变通办法,例如创建不总是使用的列,使用视图等)。一些ORML试图解决这个问题,但是它总是很快变得复杂。通过两个表之间的外键关系可以轻松地对组合进行建模,但是继承要困难得多。


81

简而言之,我会同意“优先考虑组成而不是继承”,但对我而言,这听起来常常像是“优先考虑土豆而不是可口可乐”。有继承的地方和组成的地方。您需要了解差异,然后这个问题就会消失。对我来说真正的意思是“如果您要使用继承-再想一想,您需要组合”。

当您想吃东西时,您应该选择土豆而不是可口可乐,而当您想要喝时,您应该更喜欢可口可乐。

创建子类不仅仅意味着调用超类方法的简便方法。当子类“ is-a”超类在结构上和功能上都可以使用时,您应该使用继承。如果不是这种情况-它不是继承,而是其他东西。合成是指您的对象由另一个组成或与它们有某种关系。

因此对我来说,好像某人不知道是否需要继承或构成,真正的问题是他不知道自己是否想喝酒或吃东西。多考虑您的问题领域,更好地理解它。


5
适合正确工作的正确工具。锤子可能比扳手更能砸东西,但这并不意味着人们应该将扳手视为“劣质的锤子”。当必须将添加到子类中的内容作为对象用作超类对象时,继承会很有用。例如,考虑InternalCombustionEngine具有派生类的基类GasolineEngine。后者添加了诸如火花塞之类的东西,这是基类所缺少的,但是将其用作InternalCombustionEngine会导致火花塞被使用。
2012年

61

继承是非常诱人的,尤其是从程序领域来的继承,它看起来通常看起来很优雅。我的意思是,我需要做的就是将这一功能添加到其他类中,对吗?好吧,问题之一是

继承可能是最糟糕的耦合形式

基类通过将实现细节以受保护成员的形式公开给子类来破坏封装。这会使您的系统僵化而脆弱。然而,更悲惨的缺陷是新的子类带来了继承链的所有包and和观点。

文章Inheritance is Evil:DataAnnotationsModelBinder的史诗失败,逐步介绍了C#中的一个示例。它显示了应该使用组合物时继承的使用方式以及如何将其重构。


继承不是好事还是坏事,它只是Composition的特例。实际上,子类正在实现与超类相似的功能。如果提议的子类不是重新实现而是仅使用超类的功能,则说明您不正确地使用了继承。那是程序员的错误,而不是对继承的反映。
iPherian '17

42

在Java或C#中,对象一旦实例化就无法更改其类型。

因此,如果您的对象需要显示为不同的对象或根据对象的状态而不同地表现,请使用“ 组成”:请参阅状态策略设计模式。

如果对象必须是同一类型,则使用继承或实现接口。


10
+1在越来越多的情况下,我发现继承的作用越来越少。我更喜欢共享/继承的接口和对象的组成....还是称为聚合?不要问我,我有电子工程学位!
肯尼,

我认为这是最常见的情况,其中“在继承之上构成”适用,因为两者在理论上都是合适的。例如,在营销系统中,您可能具有的概念Client。然后,PreferredClient弹出一个新的概念。应该PreferredClient继承Client吗?首选客户毕竟是“客户”,不是吗?好吧,不是那么快...就像您说的那样,对象无法在运行时更改其类。您将如何模拟client.makePreferred()操作?也许答案在于使用缺少概念的合成,Account也许吗?
plalx

而不是不同类型的Client课程,也许只是一个封装的概念Account可能是一个StandardAccount或一个PreferredAccount...
plalx

40

在这里找不到满意的答案,所以我写了一个新的答案。

了解为什么“ 更喜欢继承而不愿继承”,我们首先需要找回在这个简短成语中省略的假设。

继承有两个好处:子类型化和子类化

  1. 类型化意味着符合类型(接口)签名,即一组API,并且一个子类型可以覆盖部分签名以实现子类型多态性。

  2. 子类化意味着方法实现的隐式重用。

这两个好处带来了进行继承的两个不同目的:面向子类型和面向代码重用。

如果仅代码重用是唯一目的,则子类提供的功能可能比他所需要的多,即子类的某些公共方法对子类没有多大意义。在这种情况下,代替偏袒继承组合物,组合物被要求。这也是“ is-a”与“ has-a”概念的来源。

因此,只有在打算使用子类型化时,即以后以多态的方式使用新类时,我们才面临选择继承或组合的问题。这是在所讨论的简短习语中被忽略的假设。

子类型要符合类型签名,这意味着组合程序必须始终公开不少于该类型的API。现在开始权衡取舍:

  1. 继承提供了直接的代码重用(如果不被覆盖的话),而编写必须重新编码每个API,即使这只是委托的简单工作。

  2. 继承提供简单的开递归经由内部多态性位点this,即,调用重写方法(或者甚至类型中另一个成员函数),可以是公共或私有的(虽然泄气)。可以通过composition模拟开放递归,但是这需要额外的精力,并且可能并不总是可行的(?)。重复问题的答案与此类似。

  3. 继承公开受保护的成员。这破坏了父类的封装,并且如果由子类使用,则会在子代及其父代之间引入另一个依赖关系。

  4. 合成具有控制反转的功能,并且它的依赖项可以动态注入,如装饰器模式代理模式所示

  5. 组合具有面向组合程序的编程的好处,即以类似于复合模式的方式工作。

  6. 编写之后立即编写接口

  7. 组合具有易于多重继承的优点。

考虑到上述折衷因素,因此我们更倾向于使用组合而不是继承。但是对于紧密相关的类,即当隐式代码重用确实能带来好处,或者需要开放递归的神奇力量时,继承将是选择。


34

我个人学会了始终偏向于继承而不是继承。没有可以通过继承解决的程序化问题,而不能通过组合解决。尽管在某些情况下可能必须使用Interfaces(Java)或Protocols(Obj-C)。由于C ++一无所知,因此您必须使用抽象基类,这意味着您无法完全摆脱C ++中的继承。

组合通常更合乎逻辑,它提供更好的抽象,更好的封装,更好的代码重用(尤其是在非常大的项目中),并且仅因为您在代码中的任何地方进行了孤立的更改,就不太可能在远处破坏任何内容。这也使坚持“ 单一责任原则 ” 变得更加容易,该原则通常被总结为“ 班级变更的理由永远不只一个。 ”,这意味着每个类别的存在都是出于特定的目的,因此应该仅具有与其用途直接相关的方法。另外,由于继承树很浅,即使您的项目开始变得很大,也更容易保留概述。许多人认为继承代表了我们现实世界很好,但这不是事实。现实世界中使用的构成多于继承。几乎可以握在手中的每个现实对象都是由其他较小的现实对象组成的。

但是,在组合方面也有缺点。如果您完全跳过继承而只关注合成,您会注意到,您经常不得不编写一些额外的代码行,如果您使用了继承,则这些代码行是不必要的。您有时还被迫重复自己,这违反了 DRY原则(干=不要重复自己)。同样,组合通常需要委托,而一个方法只是调用另一个对象的另一个方法,而此调用周围没有其他代码。这样的“双重方法调用”(可能会很容易地扩展为三重或四重方法调用,甚至更远),其性能比继承要差得多,在继承中,您仅继承父方法。调用继承的方法可能与调用非继承的方法一样快,或者可能稍慢一些,但通常仍比两个连续的方法调用快。

您可能已经注意到,大多数OO语言不允许多重继承。虽然在很多情况下多重继承可以真正为您带来收益,但是这些都是例外,而不是规则。每当您遇到“多重继承将是解决此问题的一个非常酷的功能”的情况时,通常您都应该重新考虑继承,因为即使这样,它也可能需要几个额外的代码行,基于组合的解决方案通常会变得更优雅,更灵活且更适合未来。

继承确实是一个很酷的功能,但是恐怕最近几年它已经被滥用了。人们将继承视为可以钉牢这一切的锤子,无论它实际上是钉子,螺钉还是完全不同的东西。


“许多人认为继承很好地代表了我们的现实世界,但这不是事实。” 这么多!与世界上几乎所有编程指南相反,从长远来看,将现实世界的对象建模为继承链可能不是一个好主意。仅当存在非常明显的,固有的,简单的is-a关系时,才应使用继承。就像TextFile是一个File
neonblitzer

25

我的一般经验法则:在使用继承之前,请考虑组合是否更有意义。

原因:子类化通常意味着更多的复杂性和联系性,即更容易更改,维护和扩展而不犯错误。

Sun的蒂姆·布德罗Tim Boudreau)给出了更加完整和具体的答案

在我看来,使用继承的常见问题是:

  • 无辜的行为会产生意想不到的结果 -经典示例是在初始化子类实例字段之前,从超类构造函数调用可重写方法。在理想世界中,没有人会这样做。这不是一个完美的世界。
  • 它为子类提供了不正确的诱惑,使他们无法对方法调用的顺序进行假设,并且这种假设-如果超类可能随时间演变,则这种假设往往不稳定。另请参阅我的烤面包机和咖啡壶类比
  • 类变得更重 -您不一定知道您的超类在其构造函数中正在执行什么工作,或者将使用多少内存。因此,构造一些无辜的轻量级对象可能比您想象的要昂贵得多,而且如果超类不断发展,这种情况可能会随着时间而改变。
  • 它鼓励子类的爆炸式增长。类加载会花费时间,更多类会消耗内存。在处理NetBeans规模的应用程序之前,这可能不是问题,但是在那儿,我们遇到了一些实际问题,例如菜单速度慢,因为菜单的首次显示会触发大量的类加载。我们通过使用更具声明性的语法和其他技术来解决此问题,但这也需要花费时间来解决。
  • 这使得以后更改内容变得更加困难 -如果您已公开一个类,则交换超类将破坏子类-这是一个选择,一旦您将代码公开,便成为您的选择。因此,如果您不更改超类的实际功能,则可以在以后使用时有更大的自由来进行更改,而不必扩展所需的内容。以子类化JPanel为例-这通常是错误的。如果子类在某个地方是公共的,则您将永远没有机会重新审视该决定。如果以JComponent getThePanel()的形式访问它,则仍然可以执行此操作(提示:以您的API公开内部组件的模型)。
  • 对象层次结构无法扩展(或者使它们后来扩展比预先计划要困难得多) -这是经典的“层数过多”问题。我将在下面进行介绍,以及AskTheOracle模式如何解决它(尽管它可能会冒犯OOP纯粹主义者)。

...

如果允许继承,那么我的看法是:

  • 除常量外,不公开任何字段
  • 方法应该是抽象的或最终的
  • 从超类构造函数不调用任何方法

...

所有这些对小型项目的影响要小于大型项目,对私人阶级的影响要小于公共项目。


25

为什么更偏重于继承而不是继承?

查看其他答案。

什么时候可以使用继承?

人们常说,当以下句子为真时,一个类Bar可以继承一个类Foo

  1. 酒吧是个傻瓜

不幸的是,仅上述测试是不可靠的。请改用以下内容:

  1. 酒吧是foo,并且
  2. 酒吧可以做foos可以做的一切。

第一测试确保了所有的吸气剂Foo在有意义Bar(=共享属性),而在第二测试确保所有制定者Foo在有意义Bar(=共享的功能)。

示例1:狗->动物

狗是动物,而狗可以做动物可以做的所有事情(例如呼吸,死亡等)。因此,该类Dog 可以继承该类Animal

示例2:圆形-/->椭圆

圆形是椭圆形,但圆形不能做椭圆形可以做的所有事情。例如,圆圈不能伸展,而椭圆可以伸展。因此,该类Circle 不能继承class Ellipse

这被称为Circle-Ellipse问题,这实际上不是问题,只是一个明确的证明,仅第一个测试不足以得出继承是可能的结论。尤其是,此示例强调了派生类应扩展基类的功能,而不是对其进行限制。否则,基类不能被多态使用。

什么时候应该使用继承?

即使可以使用继承也不意味着您应该:使用组合始终是一种选择。继承是一个功能强大的工具,允许隐式代码重用和动态分派,但是它确实有一些缺点,这就是为什么通常首选组合的原因。继承与组合之间的权衡并不明显,我认为最好在lcn的答案加以解释。

根据经验,当预期多态使用非常普遍时,我倾向于选择继承而不是合成,在这种情况下,动态分派的功能可以使API更具可读性和优雅性。例如,Widget在GUI框架中具有多态类,或者Node在XML库中具有多态类,使得该API具有比纯粹基于组成的解决方案更易读和直观的API。

里斯科夫替代原则

众所周知,另一种用于确定继承是否可能的方法称为Liskov替代原理

使用指针或对基类的引用的函数必须能够使用派生类的对象而不知道它

本质上,这意味着如果可以多态使用基类,则继承是可能的,我认为这等效于我们的测试“ bar是foo,bar可以完成foos可以做的一切”。


椭圆形和正方形矩形方案是不佳的例子。子类总是比其父类复杂,因此存在问题。通过反转关系可以解决该问题。椭圆从圆形派生,矩形从正方形派生。在这些情况下使用合成是非常愚蠢的。
模糊逻辑

@FuzzyLogic同意,但是实际上,我的帖子从不主张在这种情况下使用合成。我只说过,圆椭圆问题是一个很好的例子,说明为什么“ is-a”并不是一个很好的测试,可以得出结论,圆应该从椭圆中派生出来。一旦得出结论,Circle不应因为违反LSP而从Ellipse派生,那么可能的选择是反转关系,使用组合或模板类,或使用涉及其他类或辅助函数的更复杂的设计,等等...的决定显然应视具体情况而定。
鲍里斯·达尔斯泰因

1
@FuzzyLogic如果您对Circle-Ellipse的具体情况我会提出什么建议感到好奇:我会建议不要实现Circle类。反转关系的问题在于它也违反了LSP:想象一下函数computeArea(Circle* c) { return pi * square(c->radius()); }。如果传递了一个椭圆,显然会损坏(radius()甚至意味着什么?)。椭圆不是圆形,因此不应从圆形派生。
鲍里斯·达尔斯泰因

computeArea(Circle *c) { return pi * width * height / 4.0; }现在是通用的。
模糊逻辑

2
@FuzzyLogic我不同意:你意识到这意味着,类圆预期派生类椭圆形的存在,因此,提供width()height()?如果现在图书馆用户决定创建另一个名为“ EggShape”的类怎么办?是否也应源自“圆”?当然不是。卵形也不是圆形,椭圆形也不是圆形,因此任何一个都不应该从Circle派生,因为它破坏了LSP。在Circle *类上执行操作的方法对圆是什么有很强的假设,而打破这些假设几乎肯定会导致错误。
鲍里斯·达尔斯泰因

19

继承非常强大,但是您不能强制继承(请参阅:圆椭圆问题)。如果您真的不能完全确定真正的“是”子类型关系,那么最好使用组合。


15

继承在子类和超类之间建立了牢固的关系。子类必须了解超类的实现细节。当您必须考虑如何扩展超级类时,创建超级类要困难得多。您必须仔细记录类不变式,并声明内部可重写方法使用哪些其他方法。

如果层次结构确实表示“是一个关系”,则继承有时会很有用。它涉及开放式封闭原则,该原则指出,应关闭类以进行修改,但可以扩展。这样,您就可以拥有多态性。具有处理超级类型及其方法的通用方法,但是通过动态调度,将调用子类的方法。这是灵活的,并有助于创建间接功能,这在软件中必不可少(以较少的了解实现细节)。

但是,继承很容易被过度使用,并且由于类之间的依赖关系而增加了复杂性。此外,由于层和方法调用的动态选择,很难理解在程序执行期间会发生什么。

我建议使用合成作为默认设置。它更具模块化,并具有后期绑定的优势(您可以动态更改组件)。另外,单独测试这些东西也更容易。而且,如果您需要使用类中的方法,则不必强制您采用某种形式(Liskov替换原理)。


3
值得注意的是,继承不是实现多态的唯一方法。装饰图案通过合成提供多态性的外观。
BitMask777'9

1
@ BitMask777:子类型多态只是一种多态,另一种可能是参数多态,您不需要继承。同样更重要的是:在谈到继承时,一种意味着类继承;另一种是类继承。.ie您可以通过具有用于多个类的公共接口来具有子类型多态性,并且不会遇到继承问题。
egaga 2012年

2
@engaga:我认为您的评论Inheritance is sometimes useful... That way you can have polymorphism是将继承和多态性的概念紧密联系在一起(假定给定上下文的子类型)。我的评论旨在指出您在评论中需要澄清的内容:继承不是实现多态性的唯一方法,实际上,不一定是在组成和继承之间做出决定的决定性因素。
BitMask777

15

假设飞机只有两个部分:引擎和机翼。
然后,有两种方法可以设计飞机等级。

Class Aircraft extends Engine{
  var wings;
}

现在,您的飞机可以从拥有固定机翼开始,
然后随时将其更改为旋转机翼。它本质上
是带有翼的发动机。但是,如果我也想
即时更换引擎怎么办?

基类要么Engine公开一个更改器以更改其
属性,要么将我重新设计Aircraft为:

Class Aircraft {
  var wings;
  var engine;
}

现在,我也可以即时更换引擎。


您的帖子提出了我以前从未考虑过的观点-继续进行类似的由多个零件组成的机械对象的类比,在枪械之类的零件上,通常会有一个标有序列号的零件,其序列号被认为是整个枪支的重量(对于手枪,通常是枪身)。一个人可能会替换所有其他零件,但仍然拥有相同的枪支,但是如果框架破裂并且需要更换,则将新框架与原始枪支的所有其他零件组装在一起的结果将是一门新枪。请注意...
supercat 2012年

...枪支的多个零件可能标有序列号这一事实并不意味着枪支可以具有多个身份。只有机架上的序列号可以识别喷枪;任何其他零件上的序列号都标识了制造这些零件所要装配的枪,在任何给定时间可能不是装配这些零件的枪。
2012年


7

当您要“复制” /公开基类的API时,可以使用继承。当您只想“复制”功能时,请使用委托。

这样的一个示例:您想从列表中创建堆栈。堆栈仅具有弹出,推入和窥视。如果您不希望在栈中使用push_back,push_front,removeAt等功能,则不应使用继承。


7

这两种方式可以很好地生活在一起,并且实际上彼此支持。

合成只是模块化地发挥作用:您创建类似于父类的接口,创建新对象并委托对其进行调用。如果这些对象不需要彼此了解,那么它非常安全且易于使用。这里有很多可能性。

但是,如果父类出于某种原因需要为经验不足的程序员访问“子类”提供的功能,那么它似乎是使用继承的好地方。父类可以只调用它自己的抽象“ foo()”,该抽象被子类覆盖,然后可以将值提供给抽象基。

看起来不错,但是在很多情况下,最好给该类一个实现foo()的对象(甚至手动设置foo()所提供的值),而不是从某个需要继承的基类继承新类。要指定的函数foo()。

为什么?

因为继承是传递信息的不良方法

这种组合在这里具有真正的优势:这种关系可以颠倒:“父类”或“抽象工作者”可以聚合实现特定接口的任何特定“子”对象+ 可以在任何其他类型的父内部设置任何子,它是type。并且可以有任意数量的对象,例如MergeSort或QuickSort可以对实现抽象Compare接口的对象列表进行排序。换一种说法:实现“ foo()”的任何对象组和可以利用具有“ foo()”的对象的其他对象组都可以一起玩。

我可以想到使用继承的三个真正原因:

  1. 您有许多具有相同接口的类,并且希望节省时间编写它们
  2. 您必须为每个对象使用相同的基类
  3. 您需要修改私有变量,在任何情况下都不能公开

如果这些是正确的,则可能有必要使用继承。

使用原因1并没有什么不好,在对象上具有可靠的接口是非常好的事情。如果此接口简单且不会更改,则可以使用组合或继承来完成此操作。通常,继承在这里非常有效。

如果原因是2,那就有点棘手了。您真的只需要使用相同的基类吗?通常,仅使用相同的基类是不够的,但这可能是您的框架的要求,这是无法避免的设计考虑。

但是,如果要使用私有变量(情况3),则可能会遇到麻烦。如果您认为全局变量不安全,则应考虑使用继承来访问也不安全的私有变量。提醒您,全局变量并不全是坏的-数据库本质上是一大组全局变量。但是,如果可以处理,那就很好了。


7

为了从不同的角度为新程序员解决此问题:

当我们学习面向对象的编程时,通常会在很早的时候就讲授继承,因此它被视为解决常见问题的简单方法。

我有三个类,都需要一些通用功能。因此,如果我编写一个基类并让它们全部继承自它们,那么它们都将具有该功能,而我只需要将其维护一次即可。

听起来不错,但实际上,由于以下几个原因之一,它几乎永远不会起作用:

  • 我们发现,我们的类还有其他一些功能。如果我们向类添加功能的方式是通过继承,则我们必须做出决定-我们是否将其添加到现有的基类中,即使不是从其继承的每个类都需要该功能?我们是否创建另一个基类?但是,已经从另一个基类继承的类又如何呢?
  • 我们发现,对于从我们的基类继承的类之一,我们希望基类的行为有所不同。因此,现在我们回去修改基类,也许添加一些虚拟方法,或者更糟的是,一些代码说:“如果我继承了A型,请执行此操作,但是如果我继承了B型,请执行此操作。 。” 出于很多原因,这很糟糕。一个是,每次我们更改基类时,我们都会有效地更改每个继承的类。因此,我们实际上是在更改A,B,C和D类,因为我们需要在A类中稍有不同的行为。我们以为自己非常谨慎,可能出于与那些无关的原因而中断其中一个类类。
  • 我们可能知道为什么我们决定让所有这些类相互继承,但是对于其他必须维护我们代码的人来说,这可能(可能不会)有意义。我们可能会迫使他们做出艰难的选择-我要做的事情确实很丑陋且凌乱,无法做出我需要的更改(请参阅上一个要点),还是只是重写了其中的一部分。

最后,我们将代码束缚在一些棘手的问题上,并从中获得任何好处,只是说:“很酷,我了解了继承,现在我使用了它。” 这并不意味着居高临下,因为我们都已经做到了。但是我们所有人都这样做了,因为没有人告诉我们不要这样做。

有人向我解释“偏向于继承而不是继承”时,我回想起我每次尝试使用继承在类之间共享功能时意识到,在大多数情况下,它并不能很好地工作。

解毒剂是单一责任原则。将其视为约束。我班必须做一件事。我必须能够给我的班级一个名称,以某种方式描述它所做的一件事情。(所有情况都有例外,但是在学习时,绝对规则有时会更好。)因此,我无法编写名为的基类ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses。无论我需要什么独特的功能,都必须在其自己的类中,然后其他需要该功能的类可以依赖于该类,而不是从该类继承。

冒着过于简化的风险,这就是组合-组成多个类一起工作。一旦养成了这种习惯,我们就会发现它比使用继承要灵活,可维护和可测试得多。


类不能使用多个基类不是对继承的不良反映,而是对特定语言缺乏功能的不良反映。
iPherian '17

自撰写此答案以来,我读了“鲍勃叔叔”的这篇文章该文章解决了这种能力不足的问题。我从未使用过允许多重继承的语言。但是回头看,这个问题被标记为“不可知的语言”,我的答案假定为C#。我需要开阔眼界。
Scott Hannen

6

除了有一个考虑因素外,还必须考虑对象必须经历的继承的“深度”。超过五,六个继承层次的任何深层继承都可能导致意外的转换和装箱/拆箱问题,在这种情况下,最好改为编写对象。


6

当你有一个是,一个两个类(例如狗是犬科动物)之间的关系,你去继承。

另一方面,当您在两个班级(学生开设课程)或(老师学习课程)之间具有某种或某种形容词关系时,您选择了作文。


你说继承和继承。您不是说继承和组成吗?
trevorKirkby 2013年

不,你没有。您也可以定义一个狗的接口并让每条狗实现它,最后您将获得更多的SOLID代码。
markus 2015年

5

一种简单的理解方法是,当您需要类的对象具有与其父类相同的接口时,应使用继承,以便可以将其视为父类的对象(向上投射)。 。此外,对派生类对象的函数调用在代码中的所有地方都将保持不变,但是具体的调用方法将在运行时确定(即,低级实现不同,高级接口保持不变)。

当您不需要新类具有相同的接口时,即您希望隐藏该类的用户不需要了解的类实现的某些方面时,应使用组合。因此,组合更多地是通过支持封装(即隐藏实现)的方式,而继承是为了支持抽象(即提供某种事物的简化表示,在这种情况下,对于具有不同内部结构的各种类型,使用相同的接口)。


+1表示介面。我经常使用这种方法来隐藏现有的类,并通过模拟用于合成的对象使新类正确地进行单元测试。这要求新对象的所有者向其传递候选父类。
凯尔2014年


4

我同意@Pavel,当他说时,有很多地方可以继承,也有很多地方可以继承。

我认为如果您的回答是对上述任何一个问题的肯定,都应该使用继承。

  • 您的班级是否属于受益于多态性的结构的一部分?例如,如果您有一个Shape类,该类声明了一个名为draw()的方法,那么我们显然需要Circle和Square类成为Shape的子类,以便它们的客户端类将取决于Shape而不是特定的子类。
  • 您的班级是否需要重用另一个班级中定义的任何高级交互?该模板方法设计模式就不可能不继承来实现。我相信所有可扩展的框架都使用这种模式。

但是,如果您的意图纯粹是代码重用,那么组合很可能是更好的设计选择。


4

继承是代码重用的一种非常强大的机制。但是需要正确使用。我要说的是,如果子类也是父类的子类型,则可以正确使用继承。如上所述,李斯科夫替代原则是这里的重点。

子类与子类型不同。您可能会创建不是子类型的子类(这是您应该使用组合的时候)。要了解什么是子类型,让我们开始解释什么是类型。

当我们说数字5是整数类型时,我们说的是5属于一组可能的值(例如,请参阅Java基本类型的可能的值)。我们还指出,我可以对一组值执行有效的方法,例如加法和减法。最后我们要说明的是,有一组属性总是可以满足的,例如,如果我将值3和5相加,结果将是8。

再举一个例子,考虑一下抽象数据类型,即整数集和整数列表,它们可以保存的值限制为整数。它们都支持一组方法,例如add(newValue)和size()。它们都具有不同的属性(类不变),Set不允许重复,而List允许重复(当然,它们都满足其他属性)。

子类型也是一种类型,它与另一个类型有关系,称为父类型(或超类型)。子类型必须满足父类型的特征(值,方法和属性)。该关系意味着在期望超类型的任何上下文中,它都可以被子类型替代,而不会影响执行的行为。让我们来看一些代码来举例说明我的意思。假设我写了一个整数列表(用某种伪语言):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

然后,我将整数集写为整数列表的子类:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

我们的整数集类是整数列表的子类,但不是子类型,因为它不能满足List类的所有功能。满足方法的值和签名,但不满足属性。add(Integer)方法的行为已明显更改,没有保留父类型的属性。从您的班级客户的角度考虑。他们可能会收到一组整数,其中应有一个整数列表。客户端可能想要添加一个值并将该值添加到列表中,即使该值已存在于列表中。但是,如果值存在,她将不会得到这种行为。给她一个很大的惊喜!

这是不当使用继承的经典示例。在这种情况下,请使用合成。

(来自:正确使用继承的片段)。


3

根据我的经验,继承关系为“是”时应使用继承,继承关系为“必须”时应使用组合。即使这样,我仍然认为您应该始终倾向于合成,因为它消除了很多复杂性。


2

组成与继承继承是一个广泛的主题。对于更好的解决方案,没有真正的答案,因为我认为这完全取决于系统的设计。

通常,对象之间的关系类型会提供更好的信息以选择其中之一。

如果关系类型为“ IS-A”关系,则继承是更好的方法。否则,关系类型为“ HAS-A”关系,则组合会更好。

它完全取决于实体关系。


2

即使首选Composition,我还是要强调继承的优点和Composition的缺点。

继承的优点:

  1. 它建立逻辑“ IS A”关系。如果汽车卡车车辆的两种类型(基础类),则子类IS A基本类。

    汽车是车辆

    卡车是一辆车

  2. 通过继承,您可以定义/修改/扩展功能

    1. 基类不提供任何实现,子类必须重写complete方法(抽象)=> 您可以实现合同
    2. 基类提供默认实现,子类可以更改行为=> 您可以重新定义合同
    3. 子类通过调用super.methodName()作为第一条语句为基础类实现添加扩展,您可以扩展合同
    4. 基类定义算法的结构,子类将覆盖算法的一部分=> 您可以在不更改基类骨架的情况下实现Template_method

组成的缺点:

  1. 在继承中,子类可以直接调用基类方法,即使由于IS A关系而没有实现基类方法。如果使用组合,则必须在容器类中添加方法以公开包含的类API

例如,如果 Car包含Vehicle,并且您必须获取在Vehicle中定义的Car的价格,则您的代码将如下所示

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

我认为它不能回答问题
almanegra

您可以重新查看OP问题。我已经解决了:每种方法都有哪些取舍?
Ravindra babu

正如您提到的,您仅谈论“继承的优点和构成的缺点”,而不是EACH方法的权衡取舍,或者是您应该一个接一个地使用的情况
almanegra

优点和缺点提供了权衡,因为继承的优点是组成的缺点,而组成的缺点是继承的优点。
Ravindra babu

1

正如许多人所说,我首先要进行检查-是否存在“是”关系。如果存在,我通常会检查以下内容:

基类是否可以实例化。也就是说,基类是否可以是非抽象的。如果不是抽象的话,我通常更喜欢构图

例如1.会计师是一名雇员。但是我不会使用继承,因为可以实例化Employee对象。

例如2.书 SellingItem。SellingItem无法实例化-这是抽象概念。因此,我将使用Inheritacne。SellingItem是一个抽象基类(或接口) C#中的)

您如何看待这种方法?

此外,我支持@anon回答“ 为什么要完全使用继承?”中的内容。

使用继承的主要原因不是作为一种组合形式,而是可以获取多态行为。如果不需要多态性,则可能不应该使用继承。

@MatthieuM。在/software/12439/code-smell-inheritance-abuse/12448#comment303759_12448中

继承的问题在于它可以用于两个正交的目的:

接口(用于多态)

实现(用于代码重用)

参考

  1. 哪个班级的设计更好?
  2. 继承与聚合

1
我不确定为什么“基类是抽象的”?LSP:如果传入Poodle对象,在Dogs上运行的所有功能都将起作用吗?如果是,则可以用Poodle代替Dog,因此可以从Dog继承。
Gishu

@Gishu谢谢。我一定会研究LSP。但是在此之前,您能否提供一个“继承适当的示例,其中基类不能是抽象的”。我认为,继承仅在基类是抽象的情况下适用。如果基类需要单独实例化,则不要进行继承。也就是说,即使会计师是雇员,也不要使用继承。
LCJ 2012年

1
最近正在阅读WCF。.net框架中的一个示例是SynchronizationContext(可以实例化base +),该队列将工作排队到ThreadPool线程上。派生包括WinFormsSyncContext(排队到UI线程上)和DispatcherSyncContext(排队到WPF Dispatcher上)
Gishu 2012年

@Gishu谢谢。但是,如果您可以提供基于银行域,人力资源域,零售域或任何其他流行域的方案,则将更有帮助。
LCJ 2012年

1
抱歉。我对这些域不熟悉。如果以前的域太钝,则另一个示例是Winforms / WPF中的Control类。基本/通用控件可以实例化。派生包括列表框,文本框等。现在我想到了,Decorator Design模式是恕我直言的一个很好的例子,也很有用。装饰器从其要包装/装饰的非抽象对象派生。
Gishu 2012年

1

我看不到有人提到钻石问题,这可能是继承带来的。

乍一看,如果类B和C继承了A并且都覆盖了方法X,而第四个类D则继承了B和C,并且不覆盖X,那么应该使用哪种XD实现?

Wikipedia很好地概述了此问题中讨论的主题。


1
d继承B和C不是A.如果是这样,那么它会使用X的实现,是在A级
法布里西奥

1
@fabricio:谢谢,我编辑了文本。顺便说一句,这种情况不会出现在不允许多类继承的语言中,对吗?
Veverke

是的,你是对的..我从来没有与一个允许多重继承(按照例如在钻石问题)的工作..
法布里西奥
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.