为什么更喜欢组合而不是继承?每种方法都有哪些权衡?什么时候应该选择继承而不是组合?
当前回答
这里没有找到满意的答案,所以我写了一个新的。
为了理解为什么“更喜欢组合而不是继承”,我们需要首先找回这个缩短的习惯用法中省略的假设。
继承有两个好处:子类型化和子类化
子类型化意味着符合类型(接口)签名,即一组API,并且可以覆盖部分签名以实现子类型化多态性。子类化意味着方法实现的隐式重用。
这两个好处带来了两个不同的继承目的:面向子类型和面向代码重用。
如果代码重用是唯一的目的,那么子类化可能会给一个比他所需要的更多的东西,即父类的一些公共方法对于子类来说没有多大意义。在这种情况下,不赞成组合而不是继承,而是要求组合。这也是“is-a”与“has-a”概念的由来。
因此,只有当有了子类型,即以后以多态的方式使用新类时,我们才会面临选择继承或组合的问题。这是在所讨论的缩短成语中被省略的假设。
To子类型要符合类型签名,这意味着组合必须始终公开不少于该类型的API数量。现在开始进行权衡:
继承提供了直接的代码重用(如果不被重写),而组合必须对每个API重新编码,即使这只是一个简单的委托工作。继承通过内部多态站点this提供了直接的开放递归,即在另一个成员函数中调用重写方法(甚至类型),无论是公共的还是私有的(尽管不鼓励)。开放递归可以通过组合来模拟,但它需要额外的努力,并且可能并不总是可行的(?)。对重复问题的回答也有类似之处。继承公开受保护的成员。这打破了父类的封装,如果被子类使用,则会引入子类和父类之间的另一个依赖关系。组合具有控制反转的特性,其依赖性可以动态注入,如装饰器模式和代理模式所示。组合具有面向组合器编程的优点,即以类似于组合模式的方式工作。编程到接口后立即进行组合。组合具有易于多重继承的优点。
考虑到上述权衡,我们因此更喜欢组合而不是继承。然而,对于紧密相关的类,即当隐式代码重用真正带来好处,或者需要开放递归的魔力时,继承应该是选择。
其他回答
将遏制视为一种关系。汽车“有”发动机,人“有”名字等等。
把继承看作是一种关系。汽车“是”车辆,人“是”哺乳动物等。
我不认为这种做法值得赞扬。我直接从史蒂夫·麦康奈尔(Steve McConnell)的《代码大全》第二版第6.3节中得到了答案。
在Java或C#中,对象一旦实例化就不能更改其类型。
因此,如果您的对象需要显示为不同的对象或根据对象的状态或条件表现出不同的行为,那么请使用组合:参考状态和策略设计模式。
如果对象需要具有相同的类型,则使用继承或实现接口。
我的经验法则是:在使用继承之前,考虑组合是否更有意义。
原因:子类化通常意味着更复杂和更紧密,即更难在不出错的情况下进行更改、维护和扩展。
Sun的Tim Boudreau给出了一个更完整、更具体的答案:
在我看来,使用继承的常见问题是:无辜的行为可能会产生意想不到的结果——这方面的经典例子是调用超类中的可重写方法构造函数,在子类实例字段初始化。在一个完美的世界里,没有人会这样做。这是不是一个完美的世界。它给子类提供了不正当的诱惑,让它们对方法调用的顺序等做出假设——这种假设往往不会如果超类可以随着时间的推移而发展,那么它是稳定的。另请参见我的烤面包机和咖啡壶类比。类变得更重了——你不一定知道你的超类在其构造函数中做了什么工作,或者它需要多少内存使用。因此,构建一些无辜的轻量级对象可以比你想象的要贵得多,如果超类进化它鼓励子类的爆炸。类加载需要时间,更多的类需要内存。在你与NetBeans规模的应用程序打交道,但在那里,我们有了真正的例如,由于第一次显示菜单触发了大量的类加载。我们通过移动到更多的声明性语法和其他技术,但这需要花费时间修复。这使得以后改变事情变得更加困难——如果你公开了一个类,那么交换超类会破坏子类-这是一个选择,一旦你公开了代码,你就结婚了所以如果你不改变你的真实功能超类,如果你使用,而不是扩展你需要的东西。例如,子类化JPanel-这通常是错误的;如果子类是在公共场所,你永远不会有机会重新审视这个决定。如果它作为JComponent getThePanel()访问,您仍然可以这样做(提示:将组件的模型作为API公开)。对象层次结构不可缩放(或使其稍后缩放比提前规划要困难得多)-这是经典的“太多层”问题我将在下面讨论AskTheOracle模式如何解决它(尽管它可能冒犯OOP纯粹主义者)。...如果你允许继承遗产,我会怎么做带着一粒盐是:永远不显示字段,常量除外方法应为抽象方法或最终方法不从超类构造函数调用任何方法...所有这些都不适用于小型项目,而适用于大型项目私人课程比公共课程
继承带来了所有不可否认的好处,下面是它的一些缺点。
继承的缺点:
您不能在运行时更改从超级类继承的实现(显然是因为继承是在编译时定义的)。继承将子类暴露给其父类实现的细节,这就是为什么人们常说继承破坏了封装(从某种意义上讲,您真正需要关注的是接口而不是实现,所以通过子类重用并不总是首选的)。继承提供的紧密耦合使得子类的实现与超级类的实现紧密结合,父类实现中的任何更改都将迫使子类进行更改。子类的过度重用会使继承堆栈变得很深,也很混乱。
另一方面,通过对象获取对其他对象的引用,在运行时定义对象组合。在这种情况下,这些对象将永远无法访问彼此的受保护数据(没有封装中断),并将被迫尊重彼此的接口。在这种情况下,实现依赖性也会比继承的情况下少得多。
简单地说,实现就像你(类)应该遵循的规则
但是扩展就像对很多类使用公共代码,也许只是其中一个需要重写。