大多数设计模式书都说我们应该“赞成对象组合而不是类继承。”
但是,任何人都可以给我一个例子,继承比对象组合更好。
答案 0 :(得分:12)
继承适用于is-a
关系。它不适合has-a
关系。
由于类/组件之间的大多数关系属于has-a
桶(例如,Car
类可能不是HashMap
,但它可能有HashMap
然后,它遵循组合通常是建模类之间的关系而不是继承的更好的主意。
但这并不是说继承对某些场景没有用或者不是正确的解决方案。
答案 1 :(得分:7)
我的简单回答是,您应该将继承用于行为目的。子类应该重写方法来改变方法和对象本身的行为。
本文(采访GoF之一的Erich Gamma)清楚阐述了为什么Favor object composition over class inheritance。
答案 2 :(得分:4)
在Java中,每当从类继承时,新类也会自动成为原始类类型的子类型。由于它是一个子类型,因此需要遵守Liskov substitution principle 这个原则基本上说你必须能够在任何需要超类型的地方使用子类型。这严重限制了新继承类的行为与原始类的不同 但是,没有编译器能够让你遵守这个原则,但如果不这样做,你就会遇到麻烦,特别是当其他程序员使用你的类时。
在允许没有子类型的子类化的语言中(如CZ language),规则“赞成对象组合而不是继承”并不像Java或C#这样的语言那么重要。
答案 3 :(得分:2)
继承允许派生类型的对象几乎在任何使用基类型对象的情况下使用。作文不允许这样做。在需要这种替换时使用继承,在不需要时使用组合。
答案 4 :(得分:0)
将其视为“is-a”或“has -a”关系
在此示例中,Human “is-a” Animal,它可能会从Animal类继承不同的数据。因此使用继承:
abstract class Animal {
private String name;
public String getName(){
return name;
}
abstract int getLegCount();
}
class Dog extends Animal{
public int getLegCount(){
return 4;
}
}
class Human extends Animal{
public int getLegCount(){
return 2;
}
}
如果一个对象是另一个对象的所有者,则组合是有意义的。就像拥有Dog对象的Human对象一样。因此,在以下示例中,Human对象“具有-a” Dog对象
class Dog{
private String name;
}
class Human{
private Dog pet;
}
希望有所帮助......
答案 5 :(得分:0)
这是一个好的OOD的基本设计原则。如果在设计中使用组合而不是在策略模式中使用继承,则可以动态地“在运行时”为类分配行为。说,
interface Xable {
doSomething();
}
class Aable implements Xable { doSomething() { /* behave like A */ } }
class Bable implements Xable { doSomething() { /* behave like B */ } }
class Bar {
Xable ability;
public void setAbility(XAble a) { ability = a; }
public void behave() {
ability.doSomething();
}
}
/*now we can set our ability in runtime dynamicly */
/*somewhere in your code */
Bar bar = new Bar();
bar.setAbility( new Aable() );
bar.behave(); /* behaves like A*/
bar.setAbility( new Bable() );
bar.behave(); /* behaves like B*/
如果你确实使用了继承,那么“Bar”会在继承上“静态地”获得行为。
答案 6 :(得分:0)
子类型需要继承。考虑:
class Base {
void Foo() { /* ... */ }
void Bar() { /* ... */ }
}
class Composed {
void Foo() { mBase.Foo(); }
void Bar() { mBase.Foo(); }
private Base mBase;
}
即使Composed
支持Foo
的所有方法,也无法将其传递给期望值为Foo
的值的函数:
void TakeBase(Base b) { /* ... */ }
TakeBase(new Composed()); // ERROR
所以,如果你想要多态,你需要继承(或它的堂兄接口实现)。
答案 7 :(得分:0)
这是一个很好的问题。多年来,我一直在会议,视频,博客文章中提出要求。我听到了各种各样的答案。我听到的唯一好的答案是性能:
语言的性能差异。有时,类利用了内置引擎优化功能,而动态合成则没有。在大多数情况下,这比与类继承相关的问题要小得多,通常,您可以将性能优化所需的所有内容内联到单个类中,并在其周围包装一个工厂函数,从而获得所需的好处而无需有问题的类层次结构。
除非发现问题,否则您永远不必担心这一点。然后,您应该分析和测试性能差异,以根据需要进行明智的权衡。通常,还有其他不涉及类继承的性能优化,包括内联,方法委派,记忆纯函数等技巧。Perf会因特定的应用程序和语言引擎而异。在这里进行分析至关重要。
此外,我听到了很多常见的误解。最常见的是关于类型系统的困惑:
将类型与类进行合并(现有的答案集中在此处)。合成可以通过实现接口来满足多态性要求。类和类型是正交的,尽管在大多数支持类的语言中,子类会自动实现超类接口,所以看起来很方便。
有三个很好的理由避免类继承,并且一次又一次地出现:
大猩猩/香蕉问题
“我认为缺乏可重用性的是面向对象的语言,而不是功能性语言。因为面向对象的语言的问题在于它们具有它们所伴随的所有隐式环境。您想要一个香蕉,但是你得到的是一只大猩猩,抱着香蕉和整个丛林。” 〜乔·阿姆斯特朗(Joe Armstrong),在彼得·塞贝尔(Peter Seibel)的《工作中的编码员》中引用。
这个问题基本上是指类继承中缺乏选择性的代码重用。通过组合,您可以通过“小型,可重用的零件”方法进行软件设计,从而只选择所需的组件,而不用构建封装与某些给定功能有关的所有内容的整体设计。
脆弱的基类问题
类继承是面向对象设计中可用的最紧密的耦合,因为基类成为子类实现的一部分。这就是为什么您还会听到“四人帮”的“设计模式”经典建议的建议:“对接口进行编程,而不是对实现进行编程。”
实现继承的问题是,即使对该实现的内部细节进行最小的更改也可能会破坏子类。如果该接口是公开的,以任何方式暴露于用户区域,则可能会破坏您甚至不知道的代码。
这是类层次结构变得脆弱的原因-随着您用新用例的增长它们很难更改。
常见的克制是我们应该不断重构代码(请参阅Martin Fowler等人的极限编程,敏捷等...)。重构成功的关键在于您不能破坏事物-但正如我们刚刚看到的那样,不破坏事物就很难重构类层次结构。
原因是,在不了解用例的所有知识的情况下,不可能创建正确的类层次结构,但是在不断发展的软件中却无法做到这一点。用例一直在项目中添加或更改。
编程中还有一个发现过程,您可以在实现代码时发现正确的设计,并了解更多有关哪些有效,哪些无效的信息。但是,有了类继承,一旦有了类分类法,就将自己陷入了困境。
在开始实施之前,您需要了解这些信息,但是学习所需信息的一部分涉及构建实施。这是一个陷阱22。
必然性重复问题。这是死亡螺旋真正蔓延的地方。有时候,您实际上只想要香蕉,而不是拿着香蕉的大猩猩,以及整个丛林。因此,您将其复制并粘贴。现在,香蕉中有一个错误,因此您可以修复它。稍后,您将获得相同的错误报告并关闭它。 “我已经解决了”。然后,您将再次获得相同的错误报告。然后再次。哦哦它不是固定的。你忘了另一个香蕉! Google“复制面食”。
有时候,您确实需要在软件中使用新的用例,但是您不能更改原始的基类,因此,您将整个类的层次结构复制并粘贴到一个新的用例中,并重命名所有层次结构中所需的类,以将该新用例强制放入代码库中。 6个月后,一名新开发人员正在研究代码,并想知道要从哪个类层次结构继承而来,没人能提供一个好的答案。
根据需要进行复制会导致复制意大利面混乱,很快人们就开始扔“重写”一词,因为这没什么大不了的。这样做的问题是大多数重写项目都会失败。我可以说出几个顶尖的组织,他们目前正在维护两个开发团队,而不是一个重写团队。我曾经见过这样的组织将资金削减给彼此,也见过类似的项目消耗大量现金,以至于初创企业或小型企业的资金耗尽而倒闭。
开发人员一直低估了类继承的影响。这是一个重要的选择,并且您每次创建基类或从基类继承时,都需要意识到选择权衡。