Prefer composition over inheritance?为什么更喜欢组合而不是继承?每种方法都有哪些权衡?你什么时候应该选择继承而不是组合? 与继承相比,更喜欢组合,因为它更具延展性/更易于稍后修改,但不要使用组合-总是方法。使用组合,很容易通过依赖注入/设置器在运行中更改行为。继承更为严格,因为大多数语言不允许从多个类型派生。所以,鹅是或多或少煮熟,一旦你从类型A。 我对以上内容的酸性测试是:
例如,塞斯纳双翼飞机将暴露飞机的完整界面,如果不是更多。所以它适合从飞机上衍生出来。
例如,鸟类可能只需要飞机的飞行行为。在这种情况下,将其作为接口/类/两者都提取出来并使其成为这两个类的成员是有意义的。 更新:我刚回到我的答案,现在似乎它是不完整的,没有具体提到芭芭拉·利斯科夫的利斯科夫替代原则,作为"我应该从这个类型继承吗?" 把遏制看作是一种关系。汽车有发动机,人有名字等等。 把继承看作是一种关系。汽车是汽车,人是哺乳动物等等。 我不相信这种方法。我直接从史蒂夫·麦康奈尔完成的第二版代码中,第6.3节。 如果你理解两者的区别,就更容易解释了。 程序代码这方面的一个例子是不使用类的PHP(特别是在php5之前)。所有逻辑都编码在一组函数中。您可以包含包含助手函数等的其他文件,并通过在函数中传递数据来执行业务逻辑。随着应用程序的增长,这可能很难管理。php5试图通过提供更多面向对象的设计来解决这个问题。 遗传这鼓励使用类。继承是OO设计的三个原则之一(继承、多态、封装)。
这是工作中的继承。员工"是"个人或从个人继承。所有继承关系都是"IS-A"关系。"员工"还隐藏"人员"的"职务"属性,即"员工"。"职务"将返回员工的职务,而不是人员的职务。 作文组合优先于继承。简单地说,你会有:
组合通常是"有"或"使用"关系。在这里,员工班有一个人。它不是从人继承的,而是将Person对象传递给它,这就是为什么它"拥有"了一个人。 组合超过继承现在假设您想要创建一个管理器类型,这样您最终会得到:
然而,这个例子会很好地工作,如果人和雇员都声明了
经理对象由员工和人员组成。标题行为取自员工。这种明确的组合消除了其他事情中的歧义,您将遇到更少的错误。 尽管继承带来了不可否认的好处,但这里有一些缺点。 继承的缺点: 另一方面,对象组合是在运行时通过对象获取对其他对象的引用来定义的。在这种情况下,这些对象将永远无法访问彼此的受保护数据(没有封装中断),并且将被迫尊重彼此的接口。在这种情况下,实现依赖性也将大大低于继承的情况。 另一个非常实际的原因是,更喜欢组合而不是继承,这与您的域模型有关,并将其映射到关系数据库。将继承映射到SQL模型真的很难(最终会出现各种各样的黑客解决方法,比如创建不总是使用的列、使用视图等)。有些ORML试图解决这个问题,但它总是很快变得复杂起来。组合可以很容易地通过两个表之间的外键关系建模,但继承要困难得多。 简言之,我同意"喜欢构图胜过继承",但对我来说,常常听起来像"喜欢土豆胜过可口可乐"。这里有继承的地方和组成的地方。你需要了解差异,然后这个问题就会消失。对我来说,它真正意味着"如果你要使用继承-再想想,你很可能需要组合"。 你想吃的时候应该喜欢土豆而不是可口可乐,喝的时候应该喜欢可口可乐而不是土豆。 创建一个子类不仅仅意味着一种调用超类方法的方便方法。当子类"is-a"在结构和功能上都是超类时,当它可以用作超类时,您应该使用继承,并且您将要使用它。如果不是这样的话——这不是继承,而是其他的东西。组合是指对象由另一个对象组成,或与之有某种关系。 所以对我来说,如果有人不知道他是否需要继承或合成,真正的问题是他不知道他是想喝还是想吃。更多地思考你的问题领域,更好地理解它。 继承是相当诱人的,特别是来自程序性土地,它通常看起来很优雅。我的意思是,我只需要把这个功能添加到其他类中,对吗?好吧,其中一个问题是继承可能是最糟糕的耦合形式 您的基类通过以受保护成员的形式向子类公开实现细节来破坏封装。这会使你的系统变得僵硬和脆弱。然而,更悲剧的缺陷是新的子类带来了继承链的所有包袱和观点。 这篇文章,继承是邪恶的:数据注释的史诗性失败,在C中进行了一个例子。它显示了在应该使用组合时继承的使用,以及如何重构它。 在Java或C语言中,对象一旦被实例化,就不能改变它的类型。 因此,如果您的对象需要作为一个不同的对象出现,或者根据一个对象的状态或条件表现出不同的行为,那么使用组合:参考状态和策略设计模式。 如果对象需要是相同的类型,那么使用继承或实现接口。 这里没有找到满意的答案,所以我写了一个新的。 为了理解为什么"更喜欢组合而不是继承",我们首先需要恢复这个简短的习语中省略的假设。 继承有两个好处:子类型化和子类化 子类型化意味着符合类型(接口)签名,即一组API,并且可以覆盖部分签名以实现子类型化多态性。 子类化意味着方法实现的隐式重用。 有了这两个好处,进行继承有两个不同的目的:面向子类型和面向代码重用。 如果代码重用是唯一的目的,那么子类化可能会提供一个比他需要的更多的,即父类的一些公共方法对子类没有多大意义。在这种情况下,组合不是倾向于组合而不是继承,而是需要组合。这也是"is-a"和"has-a"概念的由来。 因此,只有当子类型是有目的的,即稍后以多态方式使用新类时,我们才会面临选择继承或组合的问题。这是在讨论中的简短习语中被省略的假设。 要使子类型符合类型签名,这意味着组合必须始终公开不少于数量的类型API。现在权衡开始: 继承提供了直接的代码重用(如果不被重写),而组合必须重新编码每个API,即使它只是一个简单的委托工作。 继承通过内部多态站点 继承公开受保护的成员。这会破坏父类的封装,如果子类使用,则会引入子类与其父类之间的另一个依赖关系。 组合具有控制反转的befit,它的依赖关系可以动态注入,如decorator模式和proxy模式所示。 组合具有面向组合器编程的优点,即以复合模式的方式工作。 组合立即跟随编程到接口。 组合具有易于多重继承的优点。 考虑到上述权衡,我们因此更喜欢组合而不是继承。然而,对于紧密相关的类,也就是说,当隐式代码重用真的有好处,或者需要开放递归的魔力时,继承将是选择。 就我个人而言,我学会了总是喜欢组合而不是继承。可以用继承解决无法解决的编程问题,但在某些情况下可能需要使用接口(Java)或协议(Obj-C)。因为C++不知道任何这样的东西,所以你必须使用抽象基类,这意味着你不能完全摆脱C++中的继承。 组合通常更符合逻辑,它提供更好的抽象、更好的封装、更好的代码重用(尤其是在非常大的项目中),并且不太可能因为在代码中的任何地方进行了单独的更改而中断任何东西。这也使得我们更容易坚持"单一责任原则",即"一个阶级永远不应该有一个以上的改变原因",也就是说每个阶级都是为了一个特定的目的而存在的,它应该只有与它的目的直接相关的方法。还有一个非常浅的继承树,即使在项目开始变得非常大的时候,也可以更容易地保持概述。许多人认为遗产很好地代表了我们的现实世界,但这不是事实。现实世界使用的组合比继承要多得多。几乎所有你能拿在手里的真实世界物体都是由其他较小的真实世界物体组成的。 不过,这种组合也有缺点。如果您完全跳过继承,只关注组合,那么您会注意到,如果您使用了继承,那么通常需要编写一些额外的代码行,而这些代码行是不必要的。你有时也被迫重复自己,这违反了干燥原则(干燥=不要重复自己)。此外,组合通常需要委托,而一个方法只是调用另一个对象的另一个方法,而没有围绕此调用的其他代码。这样的"double方法调用"(可能很容易扩展到三倍或四倍的方法调用,甚至比这还要远)的性能比继承差得多,在继承中只继承父级的方法。调用继承方法的速度可能与调用非继承方法的速度相同,也可能稍慢,但通常仍比两个连续的方法调用快。 您可能已经注意到大多数OO语言不允许多重继承。虽然有一些情况下多重继承确实可以为您购买一些东西,但是这些都是例外而不是规则。每当您遇到这样一种情况,即您认为"多重继承对于解决这个问题来说是一个非常酷的特性",通常您应该完全重新考虑继承,因为即使它可能需要一些额外的代码行,基于组合的解决方案通常也会变得更加优雅、灵活和和未来证明。 继承确实是一个很酷的功能,但我担心在过去的几年中它被过度使用了。人们把继承看作是一把可以钉住一切的锤子,不管它实际上是一根钉子、一个螺丝钉,或者是一个完全不同的东西。 我的一般经验法则是:在使用继承之前,考虑一下组合是否更有意义。 原因:子类化通常意味着更多的复杂性和连通性,也就是说,在不犯错误的情况下更难改变、维护和扩展。 《太阳报》的蒂姆·布德劳给出了一个更为完整和具体的答案:
继承是非常强大的,但是你不能强迫它(参见:圆-椭圆问题)。如果你真的不能完全确定一个真正的"is-a"子类型关系,那么最好还是用构图。 为什么更喜欢组合而不是继承? 查看其他答案。 你什么时候应该选择继承而不是组合?无论什么时候,"一个酒吧是一个foo,一个酒吧可以做一个foo能做的一切"这句话都是有意义的。 传统的智慧认为,如果"一个酒吧是一个foo"这句话是有意义的,那么这是一个很好的提示,即选择继承是合适的。例如,狗是一种动物,因此让狗类从动物继承可能是一个好的设计。 不幸的是,这种简单的"IS-A"测试并不可靠。圆椭圆问题是一个很好的反例:尽管圆是一个椭圆,但是让圆类从椭圆继承是一个坏主意,因为椭圆可以做一些事情,但圆不能做。例如,椭圆可以拉伸,但圆不能。因此,虽然可以有 这就是为什么一个更好的测试是"一个酒吧就是一个foo,一个酒吧可以做一个foo能做的一切"。这确实意味着foo可以多态使用。"is-a"测试只是多态使用的必要条件,通常意味着所有foo的getter在bar中都是有意义的。附加的"无所不能"测试意味着所有的foo设置者在bar中也有意义。当类栏"is-a"foo时,这个额外的测试通常会失败,但会向它添加一些约束,在这种情况下,您不应该使用继承,因为foo不能以多态方式使用。换句话说,继承不是关于共享属性,而是关于共享功能。派生类应该扩展基类的功能,而不是限制它。 这相当于Liskov替换原则:
继承在子类和超级类之间创建了一个强大的关系;子类必须知道超级类的实现细节。当你不得不考虑如何扩展超级类时,创建超级类要困难得多。必须仔细记录类不变量,并说明可重写方法在内部使用的其他方法。 如果层次结构真的表示IS-A关系,继承有时是有用的。它涉及开闭原则,即类应该为修改而关闭,但为扩展而打开。这样,您就可以拥有多态性;拥有一个处理超级类型及其方法的通用方法,但是通过动态调度调用子类的方法。这是灵活的,有助于创建间接寻址,这在软件中是必不可少的(对实现细节了解得更少)。 但是,继承很容易被过度使用,并且会造成额外的复杂性,在类之间存在硬依赖关系。另外,由于层和方法调用的动态选择,理解程序执行期间发生的事情变得非常困难。 我建议使用组合作为默认设置。它更模块化,并且提供了后期绑定的好处(您可以动态更改组件)。另外,单独测试更容易。如果您需要使用一个类中的方法,您就不必具有某种形式(liskov替换原则)。 你需要看一下鲍勃叔叔的类设计坚实原则中的Liskov替换原则。:) 假设一架飞机只有两部分:发动机和机翼。然后有两种方法来设计飞机类。
现在你的飞机可以从有固定机翼开始把它们变成旋转的翅膀。基本上是有翅膀的发动机。但是如果我想改变呢发动机也在飞行? 基类
现在,我也可以在飞行中更换我的发动机。 "喜欢组合而不是继承"是一个设计原则,它说当继承不合适时不要滥用它。 如果两个实体之间没有真实的层次关系,不要使用继承,而是使用组合。组合表示"有"关系。 例如,汽车有轮子、车身、发动机等,但如果你继承了这一点,它就变成了汽车是一个轮子——这是不正确的。 有关进一步的解释,请参阅首选组合而不是继承。 当您想"复制"/公开基类的API时,可以使用继承。当您只想"复制"功能时,请使用委派。 其中一个例子是:您希望从列表中创建一个堆栈。堆栈只有pop、push和peek。您不应该使用继承,因为您不希望在堆栈中使用"后推"、"前推"、"删除"等类似功能。 这两种方式可以很好地生活在一起,并相互支持。 组合只是模块化的:您创建类似于父类的接口,创建新的对象并委托对它的调用。如果这些对象不需要相互了解,那么它是非常安全和易于使用的组合。这里的可能性太多了。 但是,如果父类出于某种原因需要访问"子类"为缺乏经验的程序员提供的函数,那么它可能看起来是一个使用继承的好地方。父类只能调用它自己的抽象"foo()",子类会覆盖它,然后它可以将值赋给抽象基。 这看起来是个不错的主意,但在很多情况下,最好给类一个实现foo()的对象(或者甚至手动设置foo()提供的值),而不是从一些需要指定函数foo()的基类继承新类。 为什么? 因为继承是一种糟糕的信息移动方式。 组合在这里有一个真正的优势:关系可以逆转:"父类"或"抽象工作者"可以聚合实现特定接口的任何特定"子"对象+任何子对象都可以设置在任何其他类型的父对象中,接受它的类型。可以有任意数量的对象,例如mergesort或quicksort可以对实现抽象比较接口的任何对象列表进行排序。或者换一种说法:任何实现"foo()"的对象组和其他可以使用具有"foo()"的对象组可以一起播放。 我可以想到使用继承的三个真正原因: 如果这些是真的,那么可能需要使用继承。 使用原因1没有什么不好的地方,在对象上有一个坚实的接口是非常好的事情。这可以使用组合或继承来完成,没有问题——如果这个接口很简单并且没有改变。在这里继承通常是相当有效的。 如果原因是数字2,那就有点棘手了。您真的只需要使用同一个基类吗?一般来说,仅仅使用同一个基类是不够的,但这可能是您的框架的一个要求,这是一个无法避免的设计考虑。 但是,如果您想使用私有变量(案例3),那么您可能遇到了麻烦。如果您认为全局变量不安全,那么您应该考虑使用继承来访问私有变量也不安全。记住,全局变量并不是那么糟糕——数据库本质上是一大组全局变量。但如果你能处理好,那就很好了。 当你在两个类之间有一个is-a关系(例如狗是一只狗)时,你就去继承。 另一方面,当你在两个班(学生有课程)或(教师学习课程)之间有一个或一些形容词关系时,你选择作文。 除了需要考虑的因素外,还必须考虑对象必须经历的继承的"深度"。如果继承深度超过5或6个级别,可能会导致意外的强制转换和装箱/拆箱问题,在这种情况下,最好组合对象。 理解这一点的一个简单方法是,当需要类的对象具有与其父类相同的接口时,应该使用继承,这样它就可以被视为父类的对象(向上转换)。此外,对派生类对象的函数调用在代码中的任何地方都将保持不变,但要调用的特定方法将在运行时确定(即,低级实现不同,高级接口保持不变)。 当您不需要新类具有相同的接口时,即希望隐藏该类的用户不需要知道的该类实现的某些方面时,应该使用组合。因此,组合更多地是以支持封装(即隐藏实现)的方式进行的,而继承则是为了支持抽象(即提供某种东西的简化表示,在这种情况下,对于具有不同内部结构的一系列类型,相同的接口)。 我同意@pavel的观点,当他说,有地方可以创作,也有地方可以继承。 如果你的回答是肯定的,我想应该继承遗产。
但是,如果您的意图纯粹是代码重用,那么组合很可能是更好的设计选择。 在可以枚举不变量的地方,子类型是适当的,而且功能更强大,否则使用函数组合进行扩展。 我听过的一个经验法则是,当继承是"is-a"关系时,应该使用继承;当继承是"has-a"关系时,应该使用组合。尽管如此,我觉得你应该总是倾向于组合,因为它消除了很多复杂性。 继承是一种非常强大的代码重用机制。但需要正确使用。如果子类也是父类的子类型,我会说继承是正确使用的。如上所述,Liskov替换原理是这里的关键。 子类与子类不同。您可能会创建不属于子类型的子类(这时应该使用组合)。为了理解子类型是什么,让我们开始解释什么是类型。 当我们说数字5是整数类型时,我们声明5属于一组可能的值(作为一个例子,参见Java原语类型的可能值)。我们还声明,对于这个值,我可以执行一组有效的方法,比如加法和减法。最后,我们声明有一组始终满足的属性,例如,如果我添加值3和5,结果将得到8。 为了给出另一个例子,考虑抽象数据类型、整数集和整数列表,它们可以保存的值被限制为整数。它们都支持一组方法,如add(newvalue)和size()。它们都有不同的属性(类不变量),集合不允许重复,而列表允许重复(当然还有其他属性都满足)。 子类型也是一种类型,它与另一种类型有关系,称为父类型(或父类型)。子类型必须满足父类型的特性(值、方法和属性)。关系意味着在任何需要父类型的上下文中,它都可以被子类型替换,而不会影响执行的行为。我们来看一些代码来举例说明我所说的。假设我写一个整数列表(用某种伪语言):
然后,我将整数集写为整数列表的子类:
我们的整数集类是整数列表的一个子类,但不是子类型,因为它不满足列表类的所有特性。方法的值和签名满足,但属性不满足。add(integer)方法的行为已明显更改,不保留父类型的属性。从类的客户机的角度思考。他们可能会收到一组整数,其中需要整数列表。客户机可能希望添加一个值并将该值添加到列表中,即使该值已经存在于列表中。但如果价值存在,她就不会有这种行为。对她来说是个大惊喜! 这是继承使用不当的典型例子。在这种情况下使用合成。 (片段来自:正确使用继承)。 复合V/S继承是一个广泛的主题。没有真正的答案,因为我认为这一切都取决于系统的设计。 一般来说,对象之间的关系类型为选择对象提供了更好的信息。 如果关系类型是"is-a"关系,那么继承是更好的方法。否则关系类型为"has-a"关系,则组合将更好地接近。 它完全取决于实体关系。 要从不同的角度为新程序员解决这个问题: 当我们学习面向对象编程时,继承通常是很早就开始教的,因此它被视为解决一个常见问题的简单方法。
听起来不错,但实际上,它几乎从来都不起作用,原因如下:
最后,我们将代码绑定在一些困难的结中,除了我们可以说,"酷,我学习了继承,现在我使用了它。"这并不意味着屈尊俯就,因为我们都做到了。但我们都这样做是因为没有人告诉我们不要这样做。 当有人向我解释"赞成组合而不是继承"时,每当我试图在使用继承的类之间共享功能时,我都会想起来,并意识到大多数时候它并不真正起作用。 解药是单一责任原则。把它当作一个约束。我的班必须做一件事。我必须能够给我的班级起一个名字,以某种方式描述它所做的一件事。(每件事都有例外,但绝对规则有时在我们学习时会更好。)这说明我不能写一个名为 冒着过于简单化的风险,这就是组合-组合多个类以共同工作。一旦我们形成了这个习惯,我们就会发现它比使用继承更加灵活、可维护和可测试。 虽然构图是首选,但我想强调继承的优点和构图的缺点。 继承的好处: 它建立了一种逻辑上的"是"关系。如果汽车和卡车是两种类型的车辆(基本类),则子类是基本类。 即 汽车是一辆汽车 卡车是一辆车 通过继承,您可以定义/修改/扩展功能 组合的缺点: 例如,如果汽车包含车辆,并且您必须获得在车辆中定义的车辆价格,则您的代码将如下所示
正如许多人所说,我将首先检查是否存在"是-是"的关系。如果存在,我通常检查以下内容:
例如1。会计是雇员。但我不会使用继承,因为可以实例化雇员对象。 例如2。这本书是一本畅销书。无法实例化SellingItem-它是抽象概念。因此我将使用继承痤疮。SellingItem是一个抽象的基类(或C中的接口) 你觉得这种方法怎么样? 另外,我支持@anon answer为什么要使用继承?
@ MatthieuM。在https://softwarengineering.stackexchange.com/questions/12439/code-嗅觉继承滥用/12448 comment303759中说
参考文献 你想强迫自己(或其他程序员)坚持什么,什么时候你想让自己(或其他程序员)有更多的自由。有人认为,当你想强迫某人以某种方式处理/解决某个特定问题,这样他们就不会走错方向时,继承是有帮助的。 我没看到有人提到钻石问题,这可能与继承有关。 总而言之,如果类B和C继承了方法X和第四个类D,并且都重写了方法X,那么第四个类D继承了B和C,并且不重写X,那么应该使用X D的哪个实现? 维基百科很好地概述了这个问题中讨论的主题。 这条规则完全是胡说八道。为什么? 原因是,在每种情况下,都可以判断是使用组合还是继承。这是由一个问题的答案决定的:"某物是另一种东西吗"或"有"一些其他的东西"。 你不能"更喜欢"把某件事变成另一件事或拥有另一件事。严格的逻辑规则适用。 也没有"人为的例子",因为在任何情况下都可以给出这个问题的答案。 如果你不能回答这个问题,还有别的问题。这包括类的重叠责任通常是错误使用接口的结果,在不同的类中重写相同的代码的频率较低。 为了避免这种情况,我还建议对完全类似于其职责的类使用好的名称。 |