为什么更喜欢组合而不是继承?每种方法都有哪些权衡?什么时候应该选择继承而不是组合?
当前回答
要从新程序员的不同角度解决这个问题:
当我们学习面向对象的编程时,继承经常很早就被教授,因此它被视为一个常见问题的简单解决方案。
我有三个类都需要一些通用功能。所以如果我编写一个基类并让它们全部继承,然后它们将所有这些都有这个功能,我只需要维护一次位置
这听起来很好,但实际上它几乎从未奏效,原因如下:
我们发现,我们希望我们的类具有其他一些功能。如果我们向类添加功能的方式是通过继承,那么我们必须决定——我们是否将其添加到现有基类中,即使不是每个从其继承的类都需要该功能?我们是否创建另一个基类?但是,已经从其他基类继承的类呢?我们发现,对于从基类继承的一个类,我们希望基类的行为稍有不同。所以现在我们回过头来修改我们的基类,可能会添加一些虚拟方法,或者更糟的是,一些代码会说,“如果我是继承的类型A,那么就这样做,但如果我是传承的类型B,那么就那样做。”这是很糟糕的,原因很多。一个是,每次我们改变基类时,我们都在有效地改变每个继承的类。所以我们真的在改变A、B、C和D类,因为我们需要在A类中有一个稍微不同的行为。尽管我们认为我们很小心,但我们可能会因为与这些类无关的原因而破坏其中一个类。我们可能知道为什么我们决定让所有这些类彼此继承,但这对其他必须维护我们代码的人来说可能没有意义。我们可能会迫使他们做出一个艰难的选择——我是做一些非常丑陋和混乱的事情来做出我需要的改变(见前面的要点),还是只是重写一堆内容。
最后,我们把代码绑在一些困难的地方,除了说:“酷,我了解了继承,现在我使用了它。”这并不意味着居高临下,因为我们都这样做了。但我们都这么做了,因为没有人告诉我们不要这样做。
当有人向我解释“喜欢组合而不是继承”时,我回想了一下每次尝试使用继承在类之间共享功能时,我都意识到大多数时候它并没有真正发挥作用。
解药是单一责任原则。将其视为约束。我的班级必须做一件事。我必须能够给我的班级起一个名字,以某种方式描述它所做的一件事。(凡事都有例外,但当我们学习时,绝对规则有时会更好。)因此,我无法编写名为ObjectBaseThatContainsVariousFunctionsNeedByDifferentClasses的基类。我所需要的任何不同的功能都必须在它自己的类中,然后需要该功能的其他类可以依赖于该类,而不是从该类继承。
在过于简化的风险下,这就是组合——组合多个类一起工作。一旦我们养成了这个习惯,我们就会发现它比使用继承更灵活、更可维护、更可测试。
其他回答
继承是非常强大的,但你不能强迫它(参见:圆-椭圆问题)。如果你真的不能完全确定一个真正的“is-a”子类型关系,那么最好是组合。
在Java或C#中,对象一旦实例化就不能更改其类型。
因此,如果您的对象需要显示为不同的对象或根据对象的状态或条件表现出不同的行为,那么请使用组合:参考状态和策略设计模式。
如果对象需要具有相同的类型,则使用继承或实现接口。
合成与继承是一个广泛的主题。对于什么更好,没有真正的答案,因为我认为这一切都取决于系统的设计。
通常,对象之间的关系类型为选择其中一个对象提供了更好的信息。
如果关系类型是“is-A”关系,则继承是更好的方法。否则关系类型为“HAS-A”关系,则组合将更接近。
这完全取决于实体关系。
这两种方式可以很好地生活在一起,并相互支持。
组合只是将其模块化:您创建类似于父类的接口,创建新对象并将调用委托给它。如果这些对象不需要彼此了解,那么组合是非常安全且易于使用的。这里有很多可能性。
然而,如果父类出于某种原因需要访问“子类”为没有经验的程序员提供的函数,那么它可能看起来是一个使用继承的好地方。父类可以只调用它自己的抽象“foo()”,该抽象被子类覆盖,然后它可以将值赋给抽象基。
这看起来是一个不错的想法,但在许多情况下,只给类一个实现foo()的对象(或者甚至手动设置foo)提供的值)要好于从需要指定foo函数的基类继承新类。
Why?
因为继承是传递信息的一种糟糕方式。
组合在这里有一个真正的优势:关系可以颠倒:“父类”或“抽象工作者”可以聚合实现特定接口的任何特定“子”对象+任何子对象都可以设置在接受其类型的任何其他类型的父类中。而且可以有任意数量的对象,例如MergeSort或QuickSort可以对实现抽象比较接口的任何对象列表进行排序。或者换一种说法:实现“foo()”的任何一组对象和可以使用具有“foo)”的对象的其他一组对象都可以一起玩。
我可以想到使用继承的三个真正原因:
您有许多具有相同接口的类,您希望节省编写它们的时间必须为每个对象使用相同的基类您需要修改私有变量,在任何情况下都不能是公共的
如果这些都是真的,那么可能需要使用继承。
使用原因1没有什么不好的,在对象上有一个坚实的界面是非常好的。这可以使用组合或继承来完成,没有问题——如果这个接口简单且不改变。通常,继承在这里非常有效。
如果原因是第二,那就有点棘手了。你真的只需要使用相同的基类吗?一般来说,仅仅使用相同的基类是不够的,但这可能是框架的一个要求,这是一个无法避免的设计考虑。
然而,如果您想使用私有变量,即案例3,那么您可能会遇到麻烦。如果您认为全局变量不安全,那么应该考虑使用继承来访问私有变量也不安全。请注意,全局变量并不是那么糟糕——数据库本质上是一组全局变量。但如果你能处理,那就很好了。
我的经验法则是:在使用继承之前,考虑组合是否更有意义。
原因:子类化通常意味着更复杂和更紧密,即更难在不出错的情况下进行更改、维护和扩展。
Sun的Tim Boudreau给出了一个更完整、更具体的答案:
在我看来,使用继承的常见问题是:无辜的行为可能会产生意想不到的结果——这方面的经典例子是调用超类中的可重写方法构造函数,在子类实例字段初始化。在一个完美的世界里,没有人会这样做。这是不是一个完美的世界。它给子类提供了不正当的诱惑,让它们对方法调用的顺序等做出假设——这种假设往往不会如果超类可以随着时间的推移而发展,那么它是稳定的。另请参见我的烤面包机和咖啡壶类比。类变得更重了——你不一定知道你的超类在其构造函数中做了什么工作,或者它需要多少内存使用。因此,构建一些无辜的轻量级对象可以比你想象的要贵得多,如果超类进化它鼓励子类的爆炸。类加载需要时间,更多的类需要内存。在你与NetBeans规模的应用程序打交道,但在那里,我们有了真正的例如,由于第一次显示菜单触发了大量的类加载。我们通过移动到更多的声明性语法和其他技术,但这需要花费时间修复。这使得以后改变事情变得更加困难——如果你公开了一个类,那么交换超类会破坏子类-这是一个选择,一旦你公开了代码,你就结婚了所以如果你不改变你的真实功能超类,如果你使用,而不是扩展你需要的东西。例如,子类化JPanel-这通常是错误的;如果子类是在公共场所,你永远不会有机会重新审视这个决定。如果它作为JComponent getThePanel()访问,您仍然可以这样做(提示:将组件的模型作为API公开)。对象层次结构不可缩放(或使其稍后缩放比提前规划要困难得多)-这是经典的“太多层”问题我将在下面讨论AskTheOracle模式如何解决它(尽管它可能冒犯OOP纯粹主义者)。...如果你允许继承遗产,我会怎么做带着一粒盐是:永远不显示字段,常量除外方法应为抽象方法或最终方法不从超类构造函数调用任何方法...所有这些都不适用于小型项目,而适用于大型项目私人课程比公共课程