1811

为什么更喜欢组合而不是继承?每种方法都有哪些取舍?什么时候应该选择继承而不是组合?

4

32 回答 32

1330

更喜欢组合而不是继承,因为它更具延展性/以后易于修改,但不要使用始终组合的方法。通过组合,可以很容易地通过依赖注入/设置器来动态改变行为。继承更加严格,因为大多数语言不允许您从一种以上的类型派生。因此,一旦您从 TypeA 派生,鹅或多或少就已经煮熟了。

我对上述内容的酸性测试是:

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

    • 例如,塞斯纳双翼飞机将暴露飞机的完整界面,如果不是更多的话。所以这使它适合从飞机派生。
  • TypeB 是否只需要 TypeA 公开的部分/部分行为?表示需要组合。

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

更新:刚刚回到我的答案,现在看来,如果没有具体提及 Barbara Liskov 的Liskov 替换原则作为“我应该从这种类型继承吗?”的测试,它是不完整的。

于 2008-09-10T03:04:26.400 回答
508

将遏制视为一种关系。汽车“有”引擎,人“有”名字,等等。

将继承视为一种关系。汽车“是”交通工具,人“是”哺乳动物,等等。

我不相信这种方法。我直接取自Steve McConnell 编写的 Code Complete的第二版第 6.3 节

于 2008-09-08T02:09:31.180 回答
247

如果您了解差异,则更容易解释。

程序守则

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

遗产

这鼓励使用类。继承是面向对象设计的三大原则之一(继承、多态、封装)。

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

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

这是工作中的继承。Employee “是” Person 或从 Person 继承。所有继承关系都是“is-a”关系。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 组成。Title 行为取自员工。这种明确的组合消除了其他方面的歧义,您会遇到更少的错误。

于 2009-05-21T07:54:54.923 回答
185

继承提供了所有不可否认的好处,以下是它的一些缺点。

继承的缺点:

  1. 您不能在运行时更改从超类继承的实现(显然是因为在编译时定义了继承)。
  2. 继承将子类暴露给其父类实现的细节,这就是为什么经常说继承破坏了封装(从某种意义上说,您确实需要只关注接口而不是实现,因此通过子类重用并不总是首选)。
  3. 继承提供的紧密耦合使得子类的实现与超类的实现非常绑定,父实现的任何变化都会迫使子类发生变化。
  4. 通过子类化过度重用会使继承堆栈变得非常深并且非常混乱。

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

于 2009-05-21T08:34:48.603 回答
101

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

于 2008-09-08T02:48:50.183 回答
96

虽然简而言之,我会同意“更喜欢组合而不是继承”,但对我来说,这通常听起来像是“更喜欢土豆而不是可口可乐”。有继承的地方,也有组合的地方。您需要了解差异,然后这个问题就会消失。这对我来说真正意味着“如果你要使用继承——再想一想,你可能需要组合”。

当你想吃的时候,你应该喜欢土豆而不是可口可乐,而当你想喝的时候,你应该喜欢可口可乐而不是土豆。

创建子类不仅仅意味着调用超类方法的便捷方式。当子类在结构和功能上都是“是”超类时,您应该使用继承,当它可以用作超类并且您将要使用它时。如果不是这样 - 它不是继承,而是别的东西。组合是指您的对象由另一个对象组成,或者与它们有某种关系。

所以对我来说,如果有人不知道他是否需要继承或组合,真正的问题是他不知道他想喝还是想吃。多想想你的问题领域,更好地理解它。

于 2009-05-08T22:29:05.967 回答
64

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

继承可能是您可以拥有的最糟糕的耦合形式

您的基类通过以受保护成员的形式向子类公开实现细节来打破封装。这使您的系统变得僵硬和脆弱。然而,更可悲的缺陷是新的子类带来了继承链的所有包袱和意见。

文章Inheritance is Evil: The Epic Fail of the DataAnnotationsModelBinder介绍了 C# 中的一个示例。它显示了在应该使用组合时使用继承以及如何重构它。

于 2010-01-23T19:55:33.197 回答
63

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

要理解为什么“更喜欢组合而不是继承”,我们首先需要找回在这个缩短的习语中省略的假设。

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

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

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

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

如果代码重用是唯一的目的,子类化可能比他需要的多一个,即父类的一些公共方法对子类没有多大意义。在这种情况下,不是更喜欢组合而不是继承,而是需要组合。这也是“is-a”与“has-a”概念的来源。

所以只有当子类型是有目的的,即以后以多态方式使用新类时,我们才会面临选择继承或组合的问题。这是在讨论的缩短的成语中被省略的假设。

子类型是为了符合类型签名,这意味着组合必须始终公开不少于该类型的 API。现在开始权衡:

  1. 如果没有被覆盖,继承提供了直接的代码重用,而组合必须重新编码每个 API,即使它只是一个简单的委托工作。

  2. 继承通过内部多态站点提供直接的开放递归,即在另一个成员函数中this调用覆盖方法(甚至类型),无论是公共的还是私有的(尽管不鼓励)。开放递归可以通过组合来模拟,但它需要额外的努力并且可能并不总是可行的(?)。这个对重复问题的回答也有类似的内容。

  3. 继承公开受保护的成员。这打破了父类的封装,如果被子类使用,则引入了子类与其父类之间的另一个依赖关系。

  4. 组合具有控制反转的优点,其依赖可以动态注入,如装饰器模式代理模式所示。

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

  6. 组合紧随对接口的编程

  7. 组合具有易于多重继承的好处。

考虑到上述权衡,我们因此更喜欢组合而不是继承。然而对于紧密相关的类,即当隐式代码重用确实有好处,或者需要开放递归的魔力时,继承应该是选择。

于 2015-09-14T05:22:50.473 回答
48

在 Java 或 C# 中,对象一旦被实例化就不能改变其类型。

因此,如果您的对象需要根据对象状态或条件显示为不同的对象或行为不同,请使用组合:请参阅状态策略设计模式。

如果对象需要属于同一类型,则使用继承或实现接口。

于 2008-09-08T02:25:15.980 回答
48

什么时候可以使用合成?

您始终可以使用组合。在某些情况下,继承也是可能的,并且可能导致更强大和/或更直观的 API,但组合始终是一种选择。

什么时候可以使用继承?

人们常说,如果“a bar is a foo”,那么该类Bar可以继承该类Foo。不幸的是,仅此测试并不可靠,请改用以下方法:

  1. bar 是 foo,并且
  2. bar 可以做 foos 可以做的所有事情。

第一个测试确保所有getter在(= 共享属性)中Foo有意义Bar,而第二个测试确保所有setter在(= 共享功能)中Foo有意义。Bar

示例:狗/动物

狗是动物,狗可以做动物可以做的一切(例如呼吸、移动等)。因此,类Dog 可以继承类Animal

反例:圆/椭圆

圆是椭圆,但圆不能做椭圆能做的一切。例如,圆形不能拉伸,而椭圆可以。因此,类Circle 不能继承类Ellipse

这被称为圆形椭圆问题,这并不是一个真正的问题,但更多地表明“条形图是 foo”本身并不是一个可靠的测试。特别是,这个例子强调派生类应该扩展基类的功能,而不是限制它。否则,不能多态地使用基类。添加测试“bars can do foos can do”确保多态使用是可能的,相当于Liskov Substitution Principle

使用指向基类的指针或引用的函数必须能够在不知情的情况下使用派生类的对象

什么时候应该使用继承?

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

根据经验,当预计多态使用非常普遍时,我倾向于选择继承而不是组合,在这种情况下,动态调度的强大功能可以带来更具可读性和优雅的 API。例如,Widget在 GUI 框架中拥有一个多态类,或Node在 XML 库中拥有一个多态类,可以让 API 比纯粹基于组合的解决方案更具可读性和直观性。

于 2016-06-03T22:23:14.543 回答
37

就我个人而言,我学会了总是更喜欢组合而不是继承。没有任何程序问题可以用继承解决,而不能用组合解决;尽管在某些情况下您可能必须使用接口(Java)或协议(Obj-C)。由于 C++ 不知道任何此类事情,因此您必须使用抽象基类,这意味着您无法完全摆脱 C++ 中的继承。

组合通常更合乎逻辑,它提供更好的抽象、更好的封装、更好的代码重用(尤其是在非常大的项目中),并且不太可能仅仅因为您在代码的任何地方进行了孤立的更改而在远处破坏任何东西。这也使得维护“单一职责原则”变得更容易,该原则通常被概括为“一个类改变的原因不应该不止一个。 ”,这意味着每个类都是为了一个特定的目的而存在的,它应该只有与其目的直接相关的方法。即使您的项目开始变得非常大,也有一个非常浅的继承树可以更容易地保持概述。很多人认为继承代表了我们的真实世界很好,但事实并非如此。现实世界使用比继承更多的组合。几乎每个你可以握在手中的现实世界对象都是由其他更小的现实世界对象组成的。

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

您可能已经注意到大多数 OO 语言不允许多重继承。虽然在某些情况下多重继承确实可以为您带来一些东西,但这些都是例外而不是规则。每当你遇到一种情况,你认为“多重继承将是解决这个问题的一个非常酷的特性”时,你通常应该重新考虑继承,因为即使它可能需要几行额外的代码行,基于组合的解决方案通常会变得更加优雅、灵活和面向未来。

继承确实是一个很酷的功能,但恐怕它在过去几年被过度使用了。人们将继承视为可以钉住一切的一把锤子,无论它实际上是钉子、螺钉还是完全不同的东西。

于 2013-01-31T19:34:05.640 回答
26

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

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

Sun 的 Tim Boudreau 给出了更完整、更具体的回答:

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

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

...

如果您确实允许继承,我对该怎么做的看法是:

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

...

所有这些都适用于小型项目而不是大型项目,并且适用于私人课程而不是公共课程

于 2012-05-21T01:59:42.463 回答
19

继承非常强大,但不能强求(参见:圆椭圆问题)。如果你真的不能完全确定真正的“is-a”子类型关系,那么最好使用组合。

于 2008-09-08T02:29:19.357 回答
17

假设一架飞机只有两部分:发动机和机翼。
那么有两种方法来设计一个飞机类。

Class Aircraft extends Engine{
  var wings;
}

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

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

Class Aircraft {
  var wings;
  var engine;
}

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

于 2010-11-29T10:54:19.637 回答
17

继承在子类和超类之间建立了牢固的关系;子类必须知道超类的实现细节。创建超类要困难得多,当您必须考虑如何扩展它时。您必须仔细记录类不变量,并说明可覆盖方法在内部使用的其他方法。

如果层次结构确实代表一种关系,那么继承有时很有用。它与开闭原则有关,该原则指出类应该对修改关闭,但对扩展开放。这样你就可以拥有多态性;拥有一个处理超类型及其方法的泛型方法,但通过动态调度调用子类的方法。这是灵活的,并且有助于创建间接性,这在软件中是必不可少的(对实现细节知之甚少)。

但是,继承很容易被过度使用,并且会产生额外的复杂性,并且类之间存在硬依赖关系。由于层和方法调用的动态选择,理解程序执行期间发生的事情也变得非常困难。

我建议使用作曲作为默认值。它更加模块化,并提供后期绑定的好处(您可以动态更改组件)。此外,单独测试这些东西也更容易。而且,如果您需要使用类中的方法,则不必采用某种形式(Liskov Substitution Principle)。

于 2009-05-21T08:11:27.090 回答
13

你需要看看鲍勃叔叔的类设计的SOLID原则中的 Liskov 替换原则。:)

于 2010-10-05T11:57:51.317 回答
10

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

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

我有三个类都需要一些通用功能。因此,如果我编写一个基类并让它们都继承自它,那么它们都将具有该功能,我只需要在一个地方维护它。

这听起来不错,但在实践中它几乎永远不会奏效,原因如下:

  • 我们发现还有一些我们希望我们的类具有的其他功能。如果我们向类添加功能的方式是通过继承,我们必须决定——我们是否将它添加到现有的基类中,即使不是每个从它继承的类都需要该功能?我们是否创建另一个基类?但是那些已经从另一个基类继承的类呢?
  • 我们发现,对于从我们的基类继承的类中的一个,我们希望基类的行为稍有不同。所以现在我们回过头来修改我们的基类,可能会添加一些虚拟方法,或者更糟糕的是,一些代码说:“如果我继承类型 A,就这样做,但如果我继承类型 B,就这样做。” 这很糟糕,原因有很多。一个是每次我们更改基类时,我们实际上都在更改每个继承的类。因此,我们确实在更改 A、B、C 和 D 类,因为我们需要在 A 类中稍微不同的行为。尽管我们认为自己很小心,但我们可能会因为与这些类无关的原因而破坏其中一个类类。
  • 我们可能知道为什么我们决定让所有这些类相互继承,但对于必须维护我们代码的其他人来说,这可能没有(可能不会)有意义。我们可能会迫使他们做出艰难的选择——我是做一些非常丑陋和混乱的事情来做出我需要的改变(参见前面的要点),还是我只是重写一堆。

最后,我们把我们的代码打成一个个复杂的结,并没有从中得到任何好处,只是我们会说,“酷,我学会了继承,现在我使用了它。” 这并不意味着居高临下,因为我们都做到了。但我们都这样做了,因为没有人告诉我们不要这样做。

一旦有人向我解释了“优先组合而不是继承”,我每次尝试使用继承在类之间共享功能时都会回想起来,并意识到大多数时候它并不能很好地工作。

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

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

于 2016-05-13T20:24:33.783 回答
8

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

一个例子:你想从一个列表中创建一个堆栈。Stack 只有 pop、push 和 peek。你不应该使用继承,因为你不想在堆栈中使用 push_back、push_front、removeAt 等功能。

于 2009-05-07T01:43:41.060 回答
7

这两种方式可以很好地生活在一起,实际上是相互支持的。

组合只是将其模块化:您创建类似于父类的接口,创建新对象并将调用委托给它。如果这些对象不需要相互了解,那么组合是非常安全且易于使用的。这里有很多可能性。

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

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

为什么?

因为继承是移动信息的一种糟糕方式

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

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

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

如果这些都是真的,那么可能有必要使用继承。

使用原因 1 并没有什么不好,在你的对象上有一个可靠的接口是非常好的事情。这可以使用组合或继承来完成,没问题 - 如果这个接口很简单并且不会改变。通常继承在这里非常有效。

如果原因是 2 号,那就有点棘手了。你真的只需要使用相同的基类吗?一般来说,仅仅使用相同的基类是不够的,但它可能是你的框架的要求,一个无法避免的设计考虑。

但是,如果你想使用私有变量,情况 3,那么你可能会遇到麻烦。如果您认为全局变量不安全,那么您应该考虑使用继承来访问私有变量也不安全。请注意,全局变量并不是那么糟糕——数据库本质上是一大组全局变量。但如果你能应付得来,那就很好了。

于 2013-04-16T20:15:09.397 回答
6

除了是一个/有一个考虑因素之外,还必须考虑您的对象必须经历的继承的“深度”。任何超过 5 或 6 级继承深度的东西都可能导致意外的强制转换和装箱/拆箱问题,在这些情况下,组合您的对象可能是明智之举。

于 2008-09-08T03:00:08.610 回答
6

当你在两个类之间有is-a关系时(例如 dog 是犬),你就去继承。

另一方面,当您在两个班级(学生有课程)或(教师学习课程)之间有has-a或一些形容词关系时,您选择了作文。

于 2013-12-30T01:57:34.593 回答
5

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

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

于 2014-06-04T11:36:14.090 回答
4

在可以枚举不变量的情况下,子类型化是适当且更强大的,否则使用函数组合来实现可扩展性。

于 2011-12-02T07:40:20.207 回答
4

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

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

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

再举一个例子,考虑抽象数据类型,整数集和整数列表,它们可以保存的值仅限于整数。它们都支持一组方法,例如 add(newValue) 和 size()。而且它们都有不同的属性(类不变量),Sets 不允许重复,而 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);
     }
  }
}

我们的 Set of integers 类是 List of Integers 的子类,但不是子类型,因为它不满足 List 类的所有特性。方法的值和签名得到满足,但属性不满足。add(Integer) 方法的行为已经明显改变,没有保留父类型的属性。从班级客户的角度思考。他们可能会收到一组整数,其中需要一个整数列表。客户端可能希望添加一个值并将该值添加到列表中,即使该值已存在于列表中。但如果价值存在,她就不会得到这种行为。对她来说是一个很大的惊喜!

这是一个不恰当地使用继承的典型例子。在这种情况下使用组合。

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

于 2013-08-31T13:41:04.667 回答
4

我同意@Pavel,当他说,有组合的地方,也有继承的地方。

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

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

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

于 2011-12-07T10:40:47.667 回答
4

尽管 Composition 是首选,但我想强调Inheritance的优点和Composition的缺点。

继承的优点:

  1. 它建立了一个逻辑“ IS A”关系。如果CarTruckVehicle的两种类型(基类),则子类基类。

    IE

    汽车是交通工具

    卡车是车辆

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

    1. 基类没有提供实现,子类必须覆盖完整的方法(抽象)=>你可以实现一个契约
    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();
     }
} 
于 2016-09-22T09:31:50.070 回答
3

我听说的一条经验法则是,当它是“is-a”关系时应该使用继承,而当它是“has-a”时应该使用组合。即便如此,我觉得你应该总是倾向于组合,因为它消除了很多复杂性。

于 2008-09-08T02:14:41.030 回答
2

组合v / s继承是一个广泛的主题。没有更好的答案,因为我认为这完全取决于系统的设计。

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

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

它完全取决于实体关系。

于 2014-03-18T09:39:25.403 回答
2

我看没有人提到钻石问题,这可能与继承有关。

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

维基百科很好地概述了这个问题中讨论的主题。

于 2015-04-16T12:41:30.660 回答
1

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

基类是否可以实例化。即基类是否可以是非抽象的。如果它可以是非抽象的,我通常更喜欢构图

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

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

您如何看待这种方法?

另外,我支持@anon 回答为什么要使用继承?

使用继承的主要原因不是作为一种组合形式——它是为了获得多态行为。如果您不需要多态性,您可能不应该使用继承。

@MatthieuM。在https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448中说

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

接口(用于多态性)

实现(用于代码重用)

参考

  1. 哪个班级设计更好?
  2. 继承与聚合
于 2012-08-01T11:15:27.120 回答
1

这条规则完全是一派胡言。为什么?

原因是在每种情况下都可以判断是使用组合还是继承。这取决于对以下问题的回答:“是某物 A 是其他东西”还是“有某物 A 是其他东西”。

你不能“更喜欢”让某物成为其他东西或拥有其他东西。适用严格的逻辑规则。

也没有“人为的例子”,因为在每种情况下都可以给出这个问题的答案。

如果你不能回答这个问题,那就有别的问题了。这包括类的重叠职责,这通常是错误使用接口的结果,较少通过在不同类中重写相同的代码。

为了避免这种情况,我还建议为类使用好名字,这与他们的职责完全相似。

于 2014-01-04T13:57:54.770 回答
0

你想强迫自己(或其他程序员)坚持什么,什么时候你想给自己(或其他程序员)更多的自由。有人认为,当您想强迫某人以某种方式处理/解决特定问题时,继承很有帮助,这样他们就不会朝着错误的方向前进。

Is-a并且Has-a是一个有用的经验法则。

于 2009-06-20T04:41:49.567 回答