为什么更喜欢组合而不是继承?每种方法都有哪些权衡?什么时候应该选择继承而不是组合?
当前回答
理解这一点的一个简单方法是,当您需要类的对象与其父类具有相同的接口时,应该使用继承,这样它就可以被视为父类的对象(上广播)。此外,派生类对象上的函数调用在代码中的任何地方都保持不变,但要调用的特定方法将在运行时确定(即,低级实现不同,高级接口保持不变)。
当您不需要新类具有相同的接口时,即您希望隐藏该类的用户不需要知道的类实现的某些方面时,应使用组合。因此,组合更多的是支持封装(即隐藏实现),而继承意味着支持抽象(即提供某种东西的简化表示,在这种情况下,相同的接口用于具有不同内部结构的一系列类型)。
其他回答
我看没有人提到钻石问题,这可能是继承带来的。
总之,如果类B和C继承了a,并且都重写了方法X,而第四个类D继承了B和C,并且没有重写X,那么应该使用X D的哪个实现?
维基百科对这个问题中讨论的主题提供了一个很好的概述。
合成与继承是一个广泛的主题。对于什么更好,没有真正的答案,因为我认为这一切都取决于系统的设计。
通常,对象之间的关系类型为选择其中一个对象提供了更好的信息。
如果关系类型是“is-A”关系,则继承是更好的方法。否则关系类型为“HAS-A”关系,则组合将更接近。
这完全取决于实体关系。
要从新程序员的不同角度解决这个问题:
当我们学习面向对象的编程时,继承经常很早就被教授,因此它被视为一个常见问题的简单解决方案。
我有三个类都需要一些通用功能。所以如果我编写一个基类并让它们全部继承,然后它们将所有这些都有这个功能,我只需要维护一次位置
这听起来很好,但实际上它几乎从未奏效,原因如下:
我们发现,我们希望我们的类具有其他一些功能。如果我们向类添加功能的方式是通过继承,那么我们必须决定——我们是否将其添加到现有基类中,即使不是每个从其继承的类都需要该功能?我们是否创建另一个基类?但是,已经从其他基类继承的类呢?我们发现,对于从基类继承的一个类,我们希望基类的行为稍有不同。所以现在我们回过头来修改我们的基类,可能会添加一些虚拟方法,或者更糟的是,一些代码会说,“如果我是继承的类型A,那么就这样做,但如果我是传承的类型B,那么就那样做。”这是很糟糕的,原因很多。一个是,每次我们改变基类时,我们都在有效地改变每个继承的类。所以我们真的在改变A、B、C和D类,因为我们需要在A类中有一个稍微不同的行为。尽管我们认为我们很小心,但我们可能会因为与这些类无关的原因而破坏其中一个类。我们可能知道为什么我们决定让所有这些类彼此继承,但这对其他必须维护我们代码的人来说可能没有意义。我们可能会迫使他们做出一个艰难的选择——我是做一些非常丑陋和混乱的事情来做出我需要的改变(见前面的要点),还是只是重写一堆内容。
最后,我们把代码绑在一些困难的地方,除了说:“酷,我了解了继承,现在我使用了它。”这并不意味着居高临下,因为我们都这样做了。但我们都这么做了,因为没有人告诉我们不要这样做。
当有人向我解释“喜欢组合而不是继承”时,我回想了一下每次尝试使用继承在类之间共享功能时,我都意识到大多数时候它并没有真正发挥作用。
解药是单一责任原则。将其视为约束。我的班级必须做一件事。我必须能够给我的班级起一个名字,以某种方式描述它所做的一件事。(凡事都有例外,但当我们学习时,绝对规则有时会更好。)因此,我无法编写名为ObjectBaseThatContainsVariousFunctionsNeedByDifferentClasses的基类。我所需要的任何不同的功能都必须在它自己的类中,然后需要该功能的其他类可以依赖于该类,而不是从该类继承。
在过于简化的风险下,这就是组合——组合多个类一起工作。一旦我们养成了这个习惯,我们就会发现它比使用继承更灵活、更可维护、更可测试。
我听过的一条经验法则是,当它是一种“is-A”关系时,应该使用继承;当它是“has-A”关系的时候,应该使用组合。尽管如此,我觉得你应该始终倾向于构图,因为它消除了很多复杂性。
你什么时候可以用作文?
你可以一直使用合成。在某些情况下,继承也是可能的,并可能导致更强大和/或更直观的API,但组合始终是一种选择。
什么时候可以使用继承?
人们常说,如果“bar是foo”,那么bar类可以继承foo类。不幸的是,仅此测试并不可靠,请改用以下方法:
bar是foo,AND酒吧可以做foos所能做的一切。
第一个测试确保Foo的所有getter在Bar中都有意义(=共享的财产),而第二个测试确保所有Foo的setter都在Bar(=共享功能)中有意义。
示例:狗/动物
狗是一种动物,狗可以做动物所能做的一切(如呼吸、移动等)。因此,狗类可以继承动物类。
反例:圆/椭圆
圆是椭圆,但圆不能做椭圆所能做的一切。例如,圆不能拉伸,而椭圆可以拉伸。因此,Circle类不能继承Ellipse类。
这被称为圆椭圆问题,这并不是一个真正的问题,但更多的是表明“一个条就是一个foo”本身不是一个可靠的测试。特别是,这个例子强调了派生类应该扩展基类的功能,而不是限制它。否则,基类不能以多态的方式使用。添加测试“bar可以做foos所能做的一切”确保了多态性的使用是可能的,这相当于Liskov替换原则:
使用基类指针或引用的函数必须能够在不知道的情况下使用派生类的对象
什么时候应该使用继承?
即使你可以使用继承,也不意味着你应该这样做:使用组合总是一种选择。继承是一个强大的工具,允许隐式代码重用和动态分派,但它确实有一些缺点,这就是为什么组合经常被首选的原因。继承和组合之间的权衡并不明显,在我看来,在lcn的回答中可以得到最好的解释。
作为经验法则,当多态性使用被认为非常普遍时,我倾向于选择继承而不是组合,在这种情况下,动态分派的功能可以导致更可读和更优雅的API。例如,在GUI框架中有一个多态类Widget,或者在XML库中有一种多态类Node,这样就可以使用一个比纯基于组合的解决方案更易读、更直观的API。