28

组合和继承。

我知道它们都是在适当时选择的工具,并且上下文对于在组合和继承之间进行选择非常重要。然而,关于每种情况的适当上下文的讨论通常有点模糊。这让我开始考虑继承和多态是传统 OOP 的不同方面有多么明显。

多态性允许人们平等地指定“is-a”关系以及继承。特别是,从基类继承隐含地在该类与其子类之间创建了多态关系。然而,虽然多态可以使用纯接口来实现,但继承通过同时传递实现细节使多态关系复杂化。通过这种方式,继承与纯多态完全不同。

作为一种工具,继承为程序员提供了不同于多态性(通过纯接口)的方式,它简化了在琐碎情况下的实现重用. 然而,在大多数情况下,超类的实现细节与子类的要求存在微妙的冲突。这就是我们有“覆盖”和“成员隐藏”的原因。在这些情况下,继承提供的实现重用是通过验证状态更改和跨级联代码级别的执行路径的额外努力来购买的:子类的完整“扁平化”实现细节分布在多个类之间,这通常意味着多个文件,其中只有部分适用于所讨论的子类。在处理继承时,查看该层次结构是绝对必要的,因为如果不查看超类的代码,就无法知道哪些未覆盖的细节会影响您的状态或转移您的执行。

相比之下,组合的独占使用保证您将看到哪些状态可以被显式实例化的对象修改,这些对象的方法由您自行决定调用。仍然没有实现真正扁平化的实现(实际上甚至是不可取的,因为结构化编程的好处是实现细节的封装和抽象)但是您仍然可以重用代码,并且您只需要查看一个地方当代码行为不端时。

为了在实践中测试这些想法,避免传统的继承以结合纯基于接口的多态性和对象组合,我想知道,

有什么对象组合和接口不能实现继承可以吗?

编辑

在迄今为止的答复中,ewernli 认为没有一种技术可以用于技术专长,而另一种则没有。他后来提到了每种技术所固有的不同模式和设计方法。这是有道理的。但是,该建议使我通过询问排他使用组合和接口来代替传统继承是否会禁止使用任何主要设计模式来完善我的问题?如果是这样,在我的情况下没有等效的模式吗?

4

5 回答 5

25

从技术上讲,所有可以通过继承实现的东西也可以通过委托来实现。所以答案是“不”。

将继承转化为委托

假设我们有以下通过继承实现的类:

public class A {
    String a = "A";
    void doSomething() { .... }
    void getDisplayName() {  return a }
    void printName { System.out.println( this.getDisplayName() };   
}

public class B extends A {
    String b = "B";
    void getDisplayName() {  return a + " " + b; }
    void doSomething() { super.doSomething() ; ... }    
}

这些东西很好用,调用printNameB 的实例将"A B"在控制台中打印。

现在,如果我们用委托重写它,我们得到:

public class A {
    String a = "A";
    void doSomething() { .... }
    void getDisplayName() {  return a }
    void printName { System.out.println( this.getName() };  
}

public class B  {
    String b = "B";
    A delegate = new A();
    void getDisplayName() {  return delegate.a + " " + b; }
    void doSomething() { delegate.doSomething() ; ... } 
    void printName() { delegate.printName() ; ... }
}

我们需要printName在 B 中定义,并在实例化 B 时创建委托。调用doSomething将以与继承类似的方式工作。但是调用printName将打印"A"在控制台中。实际上,通过委托,我们失去了“this”绑定到对象实例和能够调用已被覆盖的方法的基本方法的强大概念。

这可以在支持纯委托的语言中解决。对于纯委托,委托中的“this”仍将引用 B 的实例。这意味着this.getName()将从 B 类开始方法分派。我们实现与继承相同的效果。这是在基于原型的语言中使用的机制,例如具有委托的Self具有内置功能(您可以在此处阅读继承如何在 Self 中工作)。

但是Java没有纯委托。什么时候卡住了?不,真的,我们仍然可以自己做更多的努力:

public class A implements AInterface {
    String a = "A";
    AInterface owner; // replace "this"
    A ( AInterface o ) { owner = o }
    void doSomething() { .... }
    void getDisplayName() {  return a }
    void printName { System.out.println( owner.getName() }; 
}

public class B  implements AInterface {
    String b = "B";
    A delegate = new A( this );
    void getDisplayName() {  return delegate.a + " " + b; }
    void doSomething() { delegate.doSomething() ; ... } 
    void printName() { delegate.printName() ; ... }
}

我们基本上是在重新实现内置继承提供的功能。是否有意义?不完全是。但它说明继承总是可以转换为委托。

讨论

继承的特点是基类可以调用在子类中被覆盖的方法。这就是模板模式的精髓。这样的事情不能通过委派轻松完成。另一方面,这正是使继承难以使用的原因。理解多态调度发生在哪里以及如果方法被覆盖会产生什么影响,需要精神上的扭曲。

有一些关于继承的已知陷阱以及它可能在设计中引入的脆弱性。特别是如果类层次结构不断发展。如果使用继承,也可能存在一些相等性问题。但另一方面,它仍然是解决一些问题的一种非常优雅的方式。hashCodeequals

此外,即使继承可以用委托代替,您也可以争辩说它们仍然实现不同的目的并相互补充——它们没有传达相同的意图,这不是纯粹的技术等效性所捕捉到的。

(我的理论是,当有人开始做 OO 时,我们很想过度使用继承,因为它被认为是语言的一个特性。然后我们学习委托,这是模式/方法,我们也学会了喜欢它。一段时间后,我们在两者之间找到了平衡,并培养了直觉,在哪种情况下哪个更好。好吧,正如你所看到的,我仍然喜欢两者,并且在介绍之前都值得谨慎。)

一些文献

继承和委托是增量定义和共享的替代方法。人们普遍认为,委托提供了一个更强大的模型。本文展示了一种“自然”的继承模型,它捕获了委托的所有属性。独立地,证明了对委托捕获继承能力的某些限制。最后,概述了一个完全捕获委托和继承的新框架,并探讨了这种混合模型的一些后果。

面向对象编程中最有趣的——同时也是最有问题的——概念之一是继承。继承通常被认为是区分面向对象编程与其他现代编程范式的特征,但研究人员很少就其含义和用法达成一致。[...]

由于类的强耦合和继承引起的不需要的类成员的激增,使用组合和委托的建议已变得司空见惯。文献中对相应重构的介绍可能会让人们相信这种转变是一项简单的工作。[...]

于 2010-02-10T17:03:22.760 回答
4

组合不能像继承一样搞砸生活,当快速的程序员试图通过添加方法和扩展层次结构(而不​​是考虑自然层次结构)来解决问题时

成分不能产生奇怪的钻石,导致维修团队烧夜油摸不着头脑

继承是 GOF 设计模式中讨论的本质,如果程序员首先使用组合,那么继承就不会相同。

于 2010-02-10T17:04:13.360 回答
1

考虑一个 gui 工具包。

编辑控件是一个窗口,它应该继承窗口的关闭/启用/绘制功能——它不包含一个窗口。

那么富文本控件应该包含编辑控件的保存/读取/剪切/粘贴功能,如果它仅包含一个窗口和一个编辑控件,则将很难使用。

于 2010-02-10T17:12:15.743 回答
1

我可能对此错了,但无论如何我都会说出来,如果有人有理由我错了,请回复评论,不要对我投反对票。我能想到的一种情况是继承优于组合。

假设我在项目中使用了一个封闭源代码的 Widget 库(这意味着除了记录的内容之外,实现细节对我来说是个谜)。现在假设每个小部件都可以添加子小部件。通过继承,我可以扩展 Widget 类以创建一个 CustomWidget,然后将 CustomWidget 添加为库中任何其他小部件的子小部件。然后我添加 CustomWidget 的代码将如下所示:

Widget baseWidget = new Widget();
CustomWidget customWidget = new CustomWidget();
baseWidget.addChildWidget(customWidget);

非常干净,并且符合库添加子小部件的约定。但是,对于组合,它必须是这样的:

Widget baseWidget = new Widget();
CustomWidget customWidget = new CustomWidget();
customWidget.addAsChildToWidget(baseWidget);

不那么干净,也打破了图书馆的惯例

现在我并不是说你不能通过组合来实现这一点(事实上我的例子表明你非常清楚地可以),它在所有情况下都不是理想的,并且可能导致打破惯例和其他视觉上没有吸引力的解决方法。

于 2013-03-03T03:48:51.103 回答
-1

是的。它是运行时类型识别 (RTTI)。

于 2013-04-11T05:13:16.853 回答