更喜欢组合而不是继承?

Prefer composition over inheritance?

提问人:readonly 提问时间:9/8/2008 最后编辑:Mateen Ulhaqreadonly 更新时间:3/28/2023 访问量:473508

问:

为什么更喜欢组合而不是继承?每种方法有哪些权衡?反过来的问题是:我什么时候应该选择继承而不是组合

与语言无关的 OOP 继承组合 聚合

评论

6赞 maccullt 9/10/2008
另请参阅哪个类设计更好
6赞 Tomer Ben David 8/26/2015
一句话,继承是公开的,如果你有一个公共方法,你改变了它,它改变了已发布的API。如果您有组合,并且组合的对象已更改,则不必更改已发布的 API。
0赞 Gabriel Staples 3/9/2023
这是一本关于该主题的好读物: Medium.com:组成与继承:优点和缺点

答:

557赞 Nick Zalutskiy 9/8/2008 #1

把收容看作是一种关系。一辆车“有”发动机,一个人“有”名字,等等。

把继承看作是一种关系。汽车“是”车辆,人“是”哺乳动物,等等。

我不认为这种方法是可信的。我直接从 Steve McConnellCode Complete 第二版6.3 节中获取了它。

评论

133赞 Bill K 9/17/2008
这并不总是一个完美的方法,它只是一个很好的指导方针。Liskov 替代原理更准确(失败更少)。
51赞 Nick Zalutskiy 8/15/2011
“我的车有车。”如果你把它看作是一个单独的句子,而不是在编程上下文中,那绝对没有意义。这就是这项技术的全部意义所在。如果这听起来很尴尬,那可能是错误的。
54赞 Tristan 8/26/2011
@Nick当然,但是“我的车有一个 VehicleBehavior”更有意义(我猜你的“Vehicle”类可以命名为“VehicleBehavior”)。因此,您不能根据“有”与“是”的比较来做出决定,您必须使用 LSP,否则您会犯错误
44赞 ybakos 4/1/2012
与其说是“是”,不如说是“行为像”。继承是关于继承行为,而不仅仅是语义。
13赞 Nick Bull 10/20/2017
这并不能回答这个问题。问题是“为什么”而不是“什么”。
3赞 Evrhet Milam 9/8/2008 #2

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

50赞 dance2die 9/8/2008 #3

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

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

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

评论

10赞 kenny 5/21/2009
+1 我发现继承在大多数情况下越来越少。我更喜欢共享/继承的接口和对象组合......还是叫聚合?别问我,我有EE学位!!
0赞 plalx 9/1/2015
我认为这是“组合优先于继承”的最常见情况,因为两者在理论上都是合适的。例如,在营销系统中,您可能有一个 .然后,稍后会出现一个新的概念。应该继承吗?毕竟,首选客户“就是”客户,不是吗?好吧,没那么快......就像你说的,对象不能在运行时改变它们的类。您将如何对操作进行建模?也许答案在于使用缺少概念的构图,也许?ClientPreferredClientPreferredClientClientclient.makePreferred()Account
0赞 plalx 9/1/2015
与其说有不同类型的类,不如说只有一个类封装了 which can be a or a ...ClientAccountStandardAccountPreferredAccount
20赞 yukondude 9/8/2008 #4

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

112赞 Tim Howland 9/8/2008 #5

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

6赞 Jon Limjap 9/8/2008 #6

除了 is a/has a considerations,还必须考虑对象必须经历的继承的“深度”。任何超过五到六级继承深度的东西都可能导致意外的投射和装箱/拆箱问题,在这些情况下,组合对象可能是明智的。

1440赞 Gishu 9/10/2008 #7

比起继承,更喜欢组合,因为它更具延展性/易于以后修改,但不要使用始终组合的方法。通过组合,可以很容易地使用依赖注入/Setter 即时更改行为。继承更加严格,因为大多数语言不允许从多个类型派生。因此,一旦您从 TypeA 衍生出来,鹅或多或少就会煮熟。

我对上述的酸性测试是:

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

    • 例如,一架塞斯纳双翼飞机将暴露飞机的完整界面,如果不是更多的话。因此,这使得它适合从 Airplane 派生。
  • TypeB 是否只想要 TypeA 暴露的部分/部分行为?表示需要合成。

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

更新:刚刚回到我的答案,现在看来,如果没有具体提到芭芭拉·利斯科夫(Barbara Liskov)的李斯科夫替代原则作为“我应该继承这种类型吗?

评论

110赞 Jeshurun 6/22/2011
第二个例子直接出自《Head First Design Patterns》(amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/...)一书:)我会强烈推荐这本书给任何在谷歌上搜索这个问题的人。
4赞 Tristan 8/26/2011
这很清楚,但它可能会遗漏一些东西:“TypeB 是否想公开 TypeA 的完整接口(所有公共方法不少于),以便 TypeB 可以在需要 TypeA 的地方使用?但是如果这是真的,并且TypeB也暴露了TypeC的完整接口呢?如果 TypeC 还没有建模怎么办?
8赞 supercat 12/9/2011
你提到了我认为应该是最基本的测试:“这个对象是否应该被期望(会是什么)基本类型的对象的代码使用”。如果答案是肯定的,则对象必须继承。如果没有,那么它可能不应该。如果我有我的 druthers,语言将提供一个关键字来引用“这个类”,并提供一种定义类的方法,该类的行为应该与另一个类一样,但不能替代它(这样的类将把所有“这个类”引用替换为它自己)。
34赞 Gishu 10/28/2012
@Alexey - 重点是“我可以将塞斯纳双翼飞机传递给所有期望飞机的客户而不会让他们感到惊讶吗?如果是,那么你很可能想要继承。
10赞 Stuart Wakefield 11/23/2012
实际上,我很难想出任何继承是我的答案的例子,我经常发现聚合、组合和接口会产生更优雅的解决方案。使用这些方法可以更好地解释上述许多示例......
8赞 Anzurio 5/7/2009 #8

当你想要“复制”/公开基类的 API 时,你使用继承。如果只想“复制”功能,请使用委派。

举个例子:你想从列表中创建一个堆栈。堆栈只有 pop、push 和 peek。您不应该使用继承,因为您不希望堆栈中出现 push_back、push_front、removeAt 等类型的功能。

评论

0赞 sdindiver 6/27/2020
@Anzurio....如何区分 API 与功能?根据我的说法,一个类的公开方法,我们称它们为该类的 api 方法。这些也是类的功能。如果你使用 composition,你使用 rhe 类的公共方法,我们已经称它们为 API。
1赞 Anzurio 6/27/2020
@sdindiver很抱歉造成混乱。我想说的是,当使用继承时,您将公开父类的完整 API,但是,如果子类不需要公开该完整父类的 API,则可以改用组合,以便子类可以访问父类的功能,而无需公开其完整的 API。
103赞 Pavel Feldman 5/9/2009 #9

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

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

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

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

评论

6赞 supercat 10/29/2012
为合适的工作提供合适的工具。锤子可能比扳手更擅长敲击东西,但这并不意味着人们应该将扳手视为“劣质锤子”。当添加到子类中的内容对于对象行为为超类对象是必需的时,继承会很有帮助。例如,考虑具有派生类的基类。后者添加了基类所缺少的火花塞之类的东西,但将火花塞用作火花塞会导致火花塞被使用。InternalCombustionEngineGasolineEngineInternalCombustionEngine
278赞 aleemb 5/21/2009 #10

如果你了解其中的区别,就更容易解释。

程序守则

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

遗产

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

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

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

这是工作中的继承。“is a”或继承自 。所有继承关系都是“is-a”关系。 还隐藏了 的属性,这意味着将返回 for 而不是 。EmployeePersonPersonEmployeeTitlePersonEmployee.TitleTitleEmployeePerson

组成

组成比继承更受青睐。简单地说,你会有:

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);

组合物通常是“具有”或“使用”的关系。这里的类有一个 .它不继承对象,而是获取传递给它的对象,这就是它“有一个”Person的原因。EmployeePersonPersonPerson

组合优先于继承

现在假设您要创建一个类型,以便最终得到:Manager

class Manager : Person, Employee {
   ...
}

这个例子可以正常工作,但是,如果和两者都声明了怎么办?应该返回“运营经理”还是“先生”?在组合下,这种歧义可以更好地处理:PersonEmployeeTitleManager.Title

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

该对象由 an 和 .该行为取自 。这种明确的组合消除了歧义等,您将遇到更少的错误。ManagerEmployeePersonTitleEmployee

评论

8赞 Raj Rao 11/13/2010
对于继承:没有歧义。您正在根据要求实现 Manager 类。因此,如果这是您的需求指定的,您将返回“Manager of Operations”,否则您将只使用基类的实现。此外,还可以使 Person 成为抽象类,从而确保下游类实现 Title 属性。
93赞 Raj Rao 11/13/2010
重要的是要记住,有人可能会说“组成胜于继承”,但这并不意味着“组成总是胜过继承”。“Is a”表示继承并导致代码重用。员工是一个人(员工没有一个人)。
48赞 Michael Freidgeim 7/24/2013
这个例子令人困惑。Employee 是一个人,所以它应该使用继承。在此示例中,您不应该使用组合,因为它在域模型中是错误的关系,即使从技术上讲您可以在代码中声明它。
31赞 Resigned June 2023 11/10/2014
我不同意这个例子。员工是-一个人,这是正确使用继承的教科书案例。我还认为“问题”重新定义标题字段没有意义。Employee.Title 隐藏 Person.Title 这一事实是编程质量差的标志。毕竟,“先生”和“运营经理”真的指的是一个人的同一方面(小写)吗?我会重命名 Employee.Title,从而能够引用 Employee 的 Title 和 JobTitle 属性,这两者在现实生活中都有意义。此外,没有理由经理(续...
22赞 Resigned June 2023 11/10/2014
(...continued) 从 Person 和 Employee 继承 -- 毕竟,Employee 已经从 Person 继承。在更复杂的模型中,一个人可能是经理和代理,确实可以使用多重继承(小心!),但在许多环境中,最好有一个抽象的角色类,经理(包含他/她管理的员工)和代理(包含合同和其他信息)从中继承。然后,员工是具有多个角色的人。因此,组合和继承都得到了正确的使用。
18赞 egaga 5/21/2009 #11

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

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

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

我建议使用作曲作为默认设置。它更加模块化,并具有后期绑定的优点(您可以动态更改组件)。此外,单独测试这些东西也更容易。如果你需要使用类中的方法,你不会被迫具有某种形式(Liskov 替换原理)。

评论

4赞 BitMask777 9/21/2012
值得注意的是,继承并不是实现多态性的唯一途径。装饰器图案通过组合提供多态性的外观。
1赞 egaga 9/23/2012
@BitMask777:亚型多态性只是一种多态性,另一种是参数化多态性,你不需要遗传。更重要的是:在谈论继承时,一个是指类继承;也就是说,你可以通过为多个类提供一个通用接口来获得子类型多态性,并且你不会遇到继承的问题。
2赞 BitMask777 10/2/2012
@engaga:我把你的评论解释为将继承和多态性的概念硬链接起来(在上下文中假设子类型)。我的评论旨在指出您在评论中澄清的内容:继承不是实现多态性的唯一方法,实际上在决定组合和继承之间时不一定是决定性因素。Inheritance is sometimes useful... That way you can have polymorphism
213赞 Ahmed 5/21/2009 #12

尽管继承提供了所有不可否认的好处,但这里有一些缺点。

继承的缺点:

  1. 您不能在运行时更改从超类继承的实现(显然,因为继承是在编译时定义的)。
  2. 继承将子类暴露给其父类实现的细节,这就是为什么人们常说继承会破坏封装(从某种意义上说,你真的需要只关注接口而不是实现,所以子类重用并不总是首选)。
  3. 继承提供的紧密耦合使得子类的实现与超类的实现非常紧密地联系在一起,父实现中的任何更改都将迫使子类更改。
  4. 子类的过度重用会使继承堆栈非常深,也非常混乱。

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

评论

13赞 mindplay.dk 1/17/2014
在我看来,这是更好的答案之一 - 我还要补充一点,根据我的经验,试图在组合方面重新思考你的问题,往往会导致更小、更简单、更独立、更可重用的类,具有更清晰、更小、更集中的责任范围。这通常意味着对依赖注入或模拟(在测试中)之类的事情的需求较少,因为较小的组件通常能够独立存在。只是我的经验。YMMV :-)
4赞 Salx 5/26/2016
这篇文章的最后一段真的让我印象深刻。谢谢。
1赞 Isaak Eriksson 6/24/2021
即使你提到了一个很好的技术,我认为你并没有回答这个问题。它不是将运行时与编译时进行比较,而是将继承与组合进行比较,后者重用来自 ONE 或 MANY 类的代码,并且可以覆盖或添加新逻辑。您所描述的对象组合只是通过在类中指定为属性的对象注入逻辑,这是运行时操作的绝佳替代方法。谢谢!
0赞 AlexBor 1/30/2023
继承将子类暴露给其父类实现的细节...这是通过受保护的成员吗?
0赞 user125959 6/20/2009 #13

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

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

65赞 Mike Valenty 1/24/2010 #14

继承是相当诱人的,特别是来自程序土地,它通常看起来很优雅。我的意思是我需要做的就是将这一点功能添加到其他类中,对吧?嗯,其中一个问题是

遗传可能是最糟糕的耦合形式

基类通过以受保护成员的形式向子类公开实现详细信息来中断封装。这会使您的系统变得僵硬和脆弱。然而,更悲惨的缺陷是,新的子阶级带来了继承链的所有包袱和观点。

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

评论

5赞 iPherian 4/20/2017
继承没有好坏之分,它只是构成的一个特例。事实上,子类正在实现与超类类似的功能。如果你建议的子类没有重新实现,而只是使用超类的功能,那么你就错误地使用了继承。这是程序员的错误,而不是对继承的反思。
0赞 Trevortni 5/31/2023
整个答案似乎是基于作者对私有和受保护之间区别的无知,因为它除了一个问题(理论上可能是)由不知道基本关键字引起的问题外,实际上没有其他问题。
14赞 nabeelfarid 10/5/2010 #15

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

18赞 simplfuzz 11/29/2010 #16

假设一架飞机只有两个部分:发动机和机翼。
然后有两种方法可以设计飞机类别。

Class Aircraft extends Engine{
  var wings;
}

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

要么基类公开一个赋值器来更改其
属性,要么我重新设计为:
EngineAircraft

Class Aircraft {
  var wings;
  var engine;
}

现在,我也可以即时更换发动机。

评论

0赞 supercat 8/20/2012
你的帖子提出了一个我以前没有考虑过的观点——继续说,你是具有多个部件的机械物体的类比,在枪支之类的东西上,通常有一个部件标有序列号,其序列号被认为是整个枪支的序列号(对于手枪,它通常是框架)。一个人可以更换所有其他部件并且仍然拥有相同的枪支,但如果框架破裂并需要更换,则用原始枪支的所有其他部件组装新框架的结果将是新枪。请注意...
0赞 supercat 8/20/2012
...枪支的多个部件上可能标有序列号这一事实并不意味着枪支可以具有多个身份。只有框架上的序列号可以识别喷枪;任何其他零件上的序列号标识了这些零件的制造要组装的喷枪,而这些喷枪在任何给定时间都可能不是组装它们的喷枪。
1赞 Paramvir Singh Karwal 8/31/2020
在这种特殊情况下,我绝对不建议“即时更换发动机”
4赞 Shelby Moore III 12/2/2011 #17

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

4赞 Parag 12/7/2011 #18

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

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

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

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

27赞 Peter Tseng 5/21/2012 #19

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

原因:子类化通常意味着更多的复杂性和连接性,即更难在不犯错误的情况下进行更改、维护和扩展。

Sun 的 Tim Boudreau 给出了一个更完整、更具体的答案:

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

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

...

我对该怎么做的看法,如果你允许继承,你可以 用一粒盐服用是:

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

...

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

2赞 LCJ 8/1/2012 #20

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

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

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

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

您如何看待这种方法?

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

使用继承的主要原因不是作为一种组合形式 - 这是为了让你可以获得多态行为。如果不需要多态性,则可能不应使用继承。

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

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

接口(用于多态性)

实现(用于代码重用)

参考

  1. 哪个类设计更好?
  2. 继承与聚合

评论

1赞 Gishu 8/1/2012
我不确定为什么“基类是抽象的?”出现在讨论中。LSP:如果传入贵宾犬对象,所有在狗上运行的函数都会起作用吗?如果是,那么贵宾犬可以代替狗,因此可以从狗继承。
0赞 LCJ 8/2/2012
@Gishu谢谢。我一定会研究 LSP。但在此之前,您能否提供一个“继承是正确的示例,其中基类不能是抽象的”。我认为,只有当基类是抽象的时,继承才适用。如果需要单独实例化基类,请不要进行继承。也就是说,即使会计是雇员,也不要使用继承。
1赞 Gishu 8/2/2012
最近一直在阅读 WCF。.net Framework 中的一个示例是 SynchronizationContext(可以实例化 base +),它使队列在 ThreadPool 线程上工作。派生包括 WinFormsSyncContext(队列到 UI 线程)和 DispatcherSyncContext(队列到 WPF 调度程序)
0赞 LCJ 8/2/2012
@Gishu谢谢。但是,如果您可以提供基于银行域、HR 域、零售域或任何其他流行域的方案,那将会更有帮助。
1赞 Gishu 8/2/2012
不好意思。我不熟悉这些领域。如果前一个太迟钝,另一个示例是 Winforms/WPF 中的 Control 类。可以实例化基/泛型控件。派生包括列表框、文本框等。现在我想到了,恕我直言,装饰器设计模式就是一个很好的例子,也很有用。装饰器派生自它想要包装/装饰的非抽象对象。
39赞 Mecki 2/1/2013 #21

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

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

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

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

继承确实是一个很酷的功能,但恐怕它在过去几年中被过度使用。人们把遗产看作是一把锤子,可以钉住一切,不管它实际上是钉子、螺丝钉,还是完全不同的东西。

评论

0赞 neonblitzer 5/6/2020
“许多人认为继承很好地代表了我们的现实世界,但事实并非如此。这么多!与世界上几乎所有的编程教程相反,从长远来看,将现实世界的对象建模为继承链可能是一个坏主意。只有当存在一种非常明显的、与生俱来的、简单的 is-a 关系时,才应该使用继承。Like 是一个 .TextFileFile
0赞 Scotty Jamison 4/26/2022
@neonblitzer 甚至 TextFile is-a 文件也可以通过其他技术进行建模,而不会有太多麻烦。例如,您可以将“File”设置为具有具体 TextFile 和 BinaryFile 实现的接口,或者,“File”是一个可以使用 BinaryFileBehaviors 或 TextFileBehaviors 实例实例化的类(即使用策略模式)。我已经放弃了 is-a,现在只是遵循“继承是最后的手段,在没有其他选择足够有效的情况下使用它”的口头禅。
7赞 Tero Tolonen 4/17/2013 #22

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

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

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

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

为什么?

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

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

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

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

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

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

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

但是,如果您想使用私有变量(情况 3),那么您可能会遇到麻烦。如果你认为全局变量不安全,那么你应该考虑使用继承来访问同样不安全的私有变量。请注意,全局变量并不全是坏事 - 数据库本质上是一大组全局变量。但如果你能处理它,那就很好了。

4赞 Enrique Molinari 8/31/2013 #23

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

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

当我们说数字 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) 方法的行为已明显更改,不保留父类型的属性。从班级客户的角度思考。他们可能会收到一组整数,其中需要整数列表。客户端可能希望添加一个值,并将该值添加到列表中,即使该值已存在于列表中。但如果价值存在,她就不会有这种行为。对她来说是一个很大的惊喜!

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

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

6赞 Amir Aslam 12/30/2013 #24

当你在两个类之间有一个 is-a 关系时(例如狗是犬科动物),你就会去继承。

另一方面,当你在两个班级(学生有课程)或(教师研究课程)之间有或某种形容词关系时,你选择了作文。

评论

0赞 trevorKirkby 12/30/2013
你说继承和继承。你不是说继承和组成吗?
0赞 markus 10/4/2015
不,你没有。你也可以定义一个犬类接口,让每只狗实现它,你最终会得到更多的SOLID代码。
1赞 luke1985 1/4/2014 #25

这条规则完全是无稽之谈。为什么?

原因是在每种情况下都可以判断是使用组合还是继承。 这是由一个问题的答案决定的:“是某物是别的东西”或“有” 某事 A 某事

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

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

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

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

评论

3赞 supercat 1/6/2014
你的观点很好,尽管我认为它应该措辞得更好一些;此外,还有一些边缘情况(你认为没有任何情况)。关键的一点是,在大多数情况下,该决定实际上非常简单,不应被视为判断电话。在询问 Foo 是否应该 Bar 或包含 Bar 时,真正要回答的问题是,是否有必要让任何代码在 * 内操作,同时保留其作为 .如果没有,只需封装 a 并将其公开为成员。FooBarBarFoo
0赞 luke1985 1/6/2014
不,没有临界案例。每个案例都可以通过一开始并不明显的因素来解决。这个问题需要深入分析,否则 - 武断的决定将导致问题,导致糟糕的代码。
2赞 supercat 1/7/2014
面向对象的宇宙模型应该被视为一种工具,它通常有助于解决许多现实世界的问题。然而,并非所有的实际问题都适合面向对象的模型。有时,即使继承不适合面向对象的模型,它也可能允许在不修改的情况下使用现有代码,否则需要更多返工。这种使用可能会产生一些“技术债务”,但如果程序预计在造成问题之前就已经过时,那么这种过时基本上会消除任何此类债务。
5赞 Warbo 5/30/2014
“所有现实世界的问题都可以使用面向对象的模型来解决,因为一切都符合对象的定义。所有现实世界的问题也可以用图灵机来解决,因为一切都符合图灵机的定义。不过,这并不是一个好主意。
1赞 Scott Hannen 7/29/2016
这不是规则,而是原则。如果这是一条规则,那么人们可以合乎逻辑地将其解释为,“永远,永远继承。请改为作曲。经验不足的程序员需要这种指导。他们不会凭直觉知道哪个更好,而且他们可能会倾向于继承,因为学习材料强调它。有经验的程序员也会做出错误的选择,所以这对我们所有人都有好处。
2赞 Shami Qureshi 3/18/2014 #26

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

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

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

它完全取决于实体关系。

5赞 Yash Sampat 6/4/2014 #27

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

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

评论

0赞 Kell 6/13/2014
+1 提及接口。我经常使用这种方法来隐藏现有的类,并通过模拟用于组合的对象来使我的新类能够正确地进行单元测试。这要求新对象的所有者改为将候选父类传递给它。
2赞 Veverke 4/16/2015 #28

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

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

维基百科对这个问题中讨论的主题提供了一个很好的概述。

评论

1赞 fabricio 5/13/2015
D 继承 B 和 C,而不是 A。如果是这样,那么它将使用 A 类中 X 的实现。
1赞 Veverke 5/14/2015
@fabricio:谢谢,我编辑了文本。顺便说一句,这种情况不会发生在不允许多类继承的语言中,对吗?
0赞 fabricio 5/14/2015
是的,你是对的..而且我从未使用过允许多重继承的(根据钻石问题中的示例)。
0赞 Aleksey F. 10/7/2021
这根本不是问题,因为这是扩展系统或保留负载均衡设施和功能的基本方法。这两种实现都存在,由用户或负载平衡算法/规则决定应用哪一种以及何时应用。在语言级别上,这不是问题,因为即使在您的问题中,该方法的所有实现都使用不同的限定名称进行标识。X
74赞 lcn 9/14/2015 #29

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

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

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

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

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

有了这两个好处,继承就有两个不同的目的:面向子类型和面向代码重用。

如果代码重用是唯一的目的,那么子类化可能会给人带来比他需要的更多的东西,即父类的某些公共方法对子类没有多大意义。在这种情况下,与其偏爱组合而不是继承,不如要求组合。这也是“是”与“有”概念的来源。

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

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

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

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

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

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

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

  6. 合成紧随编程到接口之后。

  7. 组合具有易于多重遗传的优点。

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

评论

1赞 wlnirvana 4/18/2022
我想你的意思是子类化需要子类型化,就像大多数 OOP 语言一样。换个表述,“子类化意味着隐式重用方法实现符合类型(接口)签名。
0赞 Trevortni 5/31/2023
第 3 点不会破坏所声称的封装,因为父级决定成员是受保护的还是私有的。您希望您的父类完全封装吗?将所有内容声明为私有:工作完成。您想将 protected 用于其存在的目的吗?然后使用它,请放心,您不会暴露任何您不打算暴露的东西。
10赞 Scott Hannen 5/14/2016 #30

对于新手程序员来说,要从不同的角度解决这个问题:

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

我有三个类,它们都需要一些通用功能。所以如果我 编写一个基类并让它们都从中继承,然后它们将 它们都具有该功能,我只需要维护一次 地方。

这听起来不错,但实际上它几乎从未奏效,原因如下:

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

最后,我们把代码打成一些困难的结,除了说,“很酷,我学会了继承,现在我用了它。这并不意味着居高临下,因为我们都做过。但我们都这样做了,因为没有人告诉我们不要这样做。

当有人向我解释“偏爱组合而不是继承”时,我回想起每次尝试使用继承在类之间共享功能时,我意识到大多数时候它并不真正有效。

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

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

评论

0赞 iPherian 4/20/2017
类不能使用多个基类并不是对继承的不良反映,而是对特定语言缺乏能力的不良反映。
0赞 Scott Hannen 4/23/2017
在写这个答案之后的时间里,我读了“鲍勃叔叔”的这篇文章,它解决了能力不足的问题。我从未使用过允许多重继承的语言。但回头看,这个问题被标记为“与语言无关”,我的答案假设是 C#。我需要拓宽我的视野。
0赞 Trevortni 5/31/2023
第二个要点似乎只是在解释覆盖的必要性,我从未听说过不提供覆盖的语言。
0赞 Scott Hannen 5/31/2023
如果事先了解我们需要覆盖哪些行为,并且组织类以使其简单,则这是有效的。否则,结果是混淆了控制流,或者我们最终仍然修改了其他类。出于正确的原因并做对了,这是可能的。更常见的是,我们通过基类将不相关的类相互耦合,然后问题开始出现。
60赞 Boris Dalstein 6/4/2016 #31

什么时候可以使用构图?

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

什么时候可以使用继承?

人们常说,如果“一根柱线是foo”,那么该类就可以继承该类。不幸的是,仅此测试并不可靠,请改用以下方法:BarFoo

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

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

示例:狗/动物

狗是一种动物,狗可以做动物能做的一切(如呼吸、移动等)。因此,该类可以继承该类。DogAnimal

反例:圆形/椭圆

圆是一个椭圆,但圆不能做椭圆可以做的所有事情。例如,圆不能拉伸,而椭圆可以。因此,该类不能继承该类。CircleEllipse

这被称为 Circle-Ellipse 问题,这并不是一个真正的问题,而更像是一个表明“柱线是 foo”本身并不是一个可靠的测试。具体而言,此示例强调派生类应扩展基类的功能,而不是限制它。否则,基类不能多态使用。添加测试“bars can do everything that foos can do”可确保多态使用是可能的,并且等价于 Liskov 替换原理

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

何时应使用继承?

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

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

评论

0赞 Fuzzy Logic 10/18/2018
圆形-椭圆和方形-矩形方案是很糟糕的例子。子类总是比它们的超类更复杂,所以问题是人为的。这个问题可以通过反转关系来解决。椭圆派生自圆形,矩形派生自正方形。在这些场景中使用构图是非常愚蠢的。
2赞 Boris Dalstein 10/18/2018
@FuzzyLogic 如果你对 Circle-Ellipse 的具体情况感到好奇:我主张不要实现 Circle 类。反转关系的问题在于,它也违反了 LSP:想象一下函数。如果传递一个椭圆,它显然是被破坏的(radius()是什么意思?椭圆不是圆,因此不应从圆派生。computeArea(Circle* c) { return pi * square(c->radius()); }
3赞 Boris Dalstein 10/19/2018
@FuzzyLogic 我不同意:你意识到这意味着 Circle 类预料到派生类 Ellipse 的存在,因此提供了 和 ?如果现在库用户决定创建另一个名为“EggShape”的类,该怎么办?它也应该派生自“Circle”吗?当然不是。蛋形不是圆,椭圆也不是圆,所以任何一个都不应该从圆派生,因为它破坏了 LSP。对 Circle* 类执行操作的方法对 Circle 是什么做出了强有力的假设,打破这些假设几乎肯定会导致错误。width()height()
2赞 Nom1fan 2/1/2022
你真棒,喜欢这个答案:)
1赞 Boris Dalstein 2/2/2022
@Nom1fan 谢谢,我很高兴这个答案很有帮助!
4赞 Ravindra babu 9/22/2016 #32

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

继承的优点:

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

    汽车是车辆

    卡车是一种交通工具

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

    1. 基类不提供任何实现,子类必须覆盖完整的方法(抽象)=>您可以实现合约
    2. 基类提供默认实现,子类可以更改行为 => 您可以重新定义合约
    3. Sub-class 通过调用 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();
     }
} 

评论

0赞 almanegra 3/15/2017
我认为它没有回答这个问题
0赞 Ravindra babu 3/15/2017
您可以重新查看 OP 问题。我已经解决了:每种方法有什么权衡?
1赞 almanegra 3/15/2017
正如你所提到的,你只谈论“继承的优点和组合的缺点”,而不是每种方法的权衡,或者你应该使用一种而不是另一种的情况
0赞 Ravindra babu 3/15/2017
优点和缺点提供了权衡,因为继承的优点是组合的缺点,而组合的缺点是继承的优点。
0赞 numan 2/3/2022
我来晚了,但是;既然组合的缺点是继承的优点,你就没有谈论继承的缺点。你只谈到了继承的优点,两次。
18赞 Scotty Jamison 3/22/2022 #33

如果你想要自 OOP 兴起以来人们一直在给出的规范的、教科书式的答案(你看到很多人在这些答案中给出了这个答案),那么应用以下规则:“如果你有 is-a 关系,请使用继承。如果你有 has-a 关系,请使用 composition”。

这是传统的建议,如果这让您满意,您可以停止阅读并继续您的快乐之路。对于其他人...

IS-A/HAS-A 比较有问题

例如:

  • 正方形是一个矩形,但是如果你的矩形类有 / 方法,那么就没有合理的方法可以在不破坏 Liskov 的替换原则的情况下进行继承。setWidth()setHeight()SquareRectangle
  • “是-a关系”通常可以改写为听起来像是“has-a”关系。例如,雇员是一个人,但一个人也具有“受雇”的就业状态。
  • 如果您不小心,IS-A 关系可能会导致令人讨厌的多重继承层次结构。毕竟,英语中没有一条规则规定一个对象只是一个东西。
  • 人们很快就把这个“规则”传递出去,但有没有人试图支持它,或者解释为什么它是一个很好的启发式方法?当然,这很符合 OOP 应该对现实世界进行建模的想法,但这本身并不是采用原则的理由。

有关此主题的更多阅读内容,请参阅 StackOverflow 问题。

要知道何时使用继承与组合,我们首先需要了解每种方法的优缺点。

实现继承的问题

其他答案在解释继承问题方面做得非常出色,所以我将尽量不在这里深入研究太多细节。但是,这里有一个简短的列表:

  • 遵循在基类方法和子类方法之间交织的逻辑可能很困难。
  • 通过调用另一个可重写的方法粗心地实现类中的一个方法将导致泄露实现细节并破坏封装,因为最终用户可能会重写您的方法并检测您何时在内部调用它。(参见“Effective Java”第 18 项)。
  • 脆弱的基础问题,它只是说,如果你的最终用户的代码在你尝试更改它们时碰巧依赖于实现细节的泄漏,它们就会中断。更糟糕的是,大多数 OOP 语言默认允许继承 - API 设计者在重构基类时需要格外小心,如果 API 设计者不会主动阻止人们从他们的公共类继承。不幸的是,脆弱的基础问题经常被误解,导致许多人不明白维护一个任何人都可以继承的类需要什么。
  • 致命的死亡钻石

构图问题

  • 它有时可能有点冗长。

就是这样。我是认真的。这仍然是一个真正的问题,有时会与 DRY 原则产生冲突,但通常并没有那么糟糕,至少与与继承相关的无数陷阱相比是这样。

什么时候应该使用继承?

下次当你为一个项目绘制你花哨的UML图时(如果你这样做了),并且你正在考虑添加一些继承,请遵循以下建议:不要。

至少,现在还没有。

继承是作为实现多态性的工具出售的,但与它捆绑在一起的是这个强大的代码重用系统,坦率地说,大多数代码都不需要它。问题是,一旦你公开了你的继承层次结构,你就会被锁定在这种特定的代码重用风格中,即使解决你的特定问题有点矫枉过正。

为了避免这种情况,我的两分钱是永远不要公开你的基类。

  • 如果需要多态性,请使用接口。
  • 如果你需要允许人们自定义你的类的行为,通过策略模式提供明确的挂钩点,这是一种更易读的方式来实现这一点,此外,保持这种 API 的稳定性更容易,因为你完全控制他们可以改变和不能改变的行为。
  • 如果您试图通过使用继承来遵循开闭原则,以避免向类添加急需的更新,请不要这样做。更新类。如果你真的拥有你受雇维护的代码的所有权,而不是试图把东西放在它的一边,你的代码库会更干净。如果您害怕引入错误,请测试现有代码。
  • 如果需要重用代码,请首先尝试使用组合函数或帮助程序函数。

最后,如果你认为没有其他好的选择,并且你必须使用继承来实现你需要的代码重用,那么你可以使用它,但是,请遵循这四个 P.A.I.L. 限制继承规则来保持理智。

  1. 使用继承作为私有实现细节。不要公开你的基类,为此使用接口。这样,您就可以根据需要自由添加或删除继承,而无需进行重大更改。
  2. 保持基类抽象。这样可以更轻松地将需要共享的逻辑与不需要共享的逻辑区分开来。
  3. 隔离基类和子类。不要让你的子类覆盖基类方法(为此使用策略模式),并避免让它们期望属性/方法彼此存在,使用其他形式的代码共享来实现这一点。使用适当的语言功能强制基类上的所有方法不可重写(在 Java 中为“final”,在 C# 中为非 virtual)。
  4. 继承是最后的手段。

特别是隔离规则听起来可能有点粗糙,但如果你自律,你会得到一些相当不错的好处。特别是,它使您可以自由地避免与上述继承相关的所有主要令人讨厌的陷阱。

  • 遵循代码要容易得多,因为它不会在基类/子类之间穿梭。
  • 如果从不使任何方法可重写,则在方法内部调用其他可重写方法时,不会意外泄漏。换句话说,您不会意外地破坏封装。
  • 脆弱的基类问题源于依赖于意外泄露的实现细节的能力。由于基类现在是孤立的,因此它不会比通过组合依赖于另一个类更脆弱。
  • 致命的死亡钻石不再是问题,因为根本不需要多层继承。如果您有抽象基类 B 和 C,它们都共享许多功能,只需将该功能从 B 和 C 移出并移动到新的抽象基类(类 D)中。由于基类都是私有实现细节,因此要弄清楚谁继承了什么,进行这些更改应该不会太难。

结论

我的主要建议是在这件事上动动脑筋。比关于何时使用继承的注意事项列表更重要的是对继承及其相关优缺点的直观理解,以及对可以使用的其他工具的良好理解,而不是继承(组合不是唯一的选择。例如,策略模式是一个了不起的工具,但经常被遗忘)。也许当你对所有这些工具都有很好的、扎实的了解时,你会选择比我建议的更频繁地使用继承,这完全没问题。至少,你正在做出一个明智的决定,而不仅仅是使用继承,因为这是你知道如何去做的唯一方法。

延伸阅读:

  • 一篇文章我写了关于这个主题的文章,更深入地探讨了这个问题,并提供了例子。
  • 一个网页,讨论继承所做的三种不同的工作,以及如何通过Go语言中的其他方式完成这些工作。
  • 将类声明为不可继承的原因列表(例如,Java 中的“final”)。
  • 约书亚·布洛赫(Joshua Bloch)所著的《有效的爪哇》(Effective Java)一书,第18项,讨论了组合而不是继承,以及继承的一些危险。
0赞 Mohamed Reda 5/16/2022 #34

简单地说,实现就像你(类)应该遵循的规则

但是 extend 就像对许多类使用通用代码一样,也许只需要覆盖其中一个类。

1赞 Alexey Nikitenko 10/11/2022 #35

只是意见,继承只应在以下情况下使用:

  • 这两个类位于同一逻辑域中
  • 子类是超类的适当子类型
  • 超类的实现对于子类是必需的或合适的
  • 子类所做的增强主要是累加的。

有时,所有这些东西都会融合在一起:

  • 更高级别的域建模
  • 框架和框架扩展
  • 差分编程