【问题标题】:Is there anything composition cannot accomplish that inheritance can?有什么组合不能实现继承可以吗?
【发布时间】:2011-01-15 08:51:22
【问题描述】:

组合和继承。

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

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

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

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

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

有什么对象组合和接口无法完成继承可以做到的事情吗?

编辑

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

【问题讨论】:

  • 就个人而言,我喜欢 mixins。 :)
  • 我没有时间检查有效的重复,但是这个继承与组合主题在 SO 上经常被访问,例如。 stackoverflow.com/questions/216523/…stackoverflow.com/questions/1598722/…One can always put a twist on this theme and call it a novel take on things... 然而,我们应该同意的一件事是,这类问题不会导致明确甚至权威的答案。也许 CW 可能是更合适的格式...
  • 我并不是要重新讨论一个已经很累的辩论。诚然,我陈述我的案例的方式几乎是上述辩论的片面样本,但我的主要兴趣是回答是否存在真正不能用组合和接口代替的继承。 ps,什么是CW格式?也许我会尝试...

标签: oop inheritance polymorphism composition


【解决方案1】:

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

将继承转化为委托

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

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() ; ... }    
}

这些东西很好用,在 B 的实例上调用 printName 将在控制台中打印 "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() ; ... }
}

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

这可以在支持纯委托的语言中解决。对于纯委托,委托中的“this”仍将引用 B 的实例。这意味着this.getName() 将从类 B 开始分派方法。我们实现与继承相同的操作。这是prototype-based 语言中使用的机制,例如Self,它具有委托具有内置功能(您可以阅读here 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() ; ... }
}

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

讨论

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

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

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

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

一些文献

继承和委托是 增量的替代方法 定义和分享。它有 普遍认为,代表团 提供了更强大的模型。这 论文证明有一个 “自然”的继承模式 捕获所有属性 代表团。独立地,确定 能力的限制 获取继承的委托是 证明了。最后,一个新的框架 这充分体现了两个代表团 并概述了继承,还有一些 这种混合的后果 模型进行了探索。

其中一个最有趣的 - 并且在 同时也是最有问题的—— 面向对象编程是 遗产。继承很常见 被视为特征 区分面向对象 其他现代编程 编程范式,但研究人员 很少就其含义和用法达成一致。 [...]

由于类的强耦合和不需要的类成员的激增导致 通过继承,使用组合和委托的建议变得司空见惯。 文献中相应重构的介绍可能会让人相信 这种转变是一项直截了当的工作。 [...]

【讨论】:

  • 这部分内容是否摘自已发表的论文?如果是这样,我会对链接或 doi 感兴趣。
  • 第一段是我的,是我的意见。然后有 3 篇与我引用摘要的主题相关的论文的参考资料。 doi 可以在所有指向portal.acm.org 的链接中找到。抱歉,如果不清楚。如果找不到 pdf 格式的文件,请告诉我。
  • 谢谢您的澄清。
  • 某事迫使我投反对票。尽管我同意继承经常被滥用,但问题/答案的基调让我感到困扰。继承是一种专门的工具。仅仅因为有人不知道如何使用它,并不意味着您将其丢弃。人们可以很容易地问:“有没有什么 Java 能做你不能用字节码做的事情?”。显然不是,但你自己强调了这个问题:“这有意义吗?不是真的。”
  • 我可能会爱上你的回答。
【解决方案2】:

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

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

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

【讨论】:

  • 将我标记为巨魔,但在向他们学习之前我犯了所有这些错误
  • 是的。过去几天我一直在修复完全依赖于继承的代码。一个基类的必要修复导致了 1000 行代码的更改。
【解决方案3】:

考虑一个 gui 工具包。

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

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

【讨论】:

  • 在讨论 GUI 类时,我认为将视觉组合与类组合混淆太容易了。可以为类似窗口的对象创建 IWindow 声明签名,并创建 Window 来实际绘制窗口和处理事件。然后创建实现 IWindow 的 IEditControl 和 EditControl。 EditControl 只是将其 IWindow 职责委托给 Window 对象。公开地,EditControl 看起来像任何其他 IWindow,并且像带有 EditControl 装饰的 Window 一样工作。最后,RichTextControl 实现 IEditControl,委托给它自己的 EditControl 对象。该模式是可链接的。
  • 没错,大多数继承示例(员工类等)最好作为组合来完成。但是 gui 工具包确实受益于继承(恕我直言)
  • 尊敬的:我的公司在 .Net 中维护着一个庞大的遗留系统;通过 MSFT 的 WinForms 示例,最初的开发人员使用大量继承来实现 UI 类。我们有 18 个独特(尽管相似)的组合框、12 种基本形式,以及 CLI 上最多 8 层的继承层次结构。我们的 UI 框架是如此的迷宫,如此的不一致和脆弱,目前的开发团队害怕对其进行最轻微的改变。但是,通常,所有这些代码都无法满足新的需求......组合意味着我们可以根据需要挑选和选择功能,而不是无休止地派生新组合。
【解决方案4】:

这点我可能错了,但我还是要说出来,如果有人有我错的原因,请回复评论,不要对我投反对票。我能想到的一种情况是继承优于组合。

假设我在一个项目中使用了一个封闭源代码的 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);

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

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

【讨论】:

  • 您强调 Widget 位于外部的封闭源代码库中。如果 Widget.addChildWidget() 采用 Widget 类型的参数,那么 IMO 继承是唯一可行的选择。该库有父母跟踪孩子,但您的组合示例需要 CustomWidgets 跟踪他们的父母,而不是被他们跟踪。 (糟糕!)如果 Widget.addChildWidget() 采用 IWidget 类型的参数,组合仍然可以工作,因为 CustomWidget 可以实现 IWidget,同时将一些行为推迟到私有 Widget。顺便说一句,接口组合的真正力量来自 IOC 和 DI。
【解决方案5】:

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

【讨论】:

  • 如果你需要 RTTI,坦率地说,你违反了 DIP。但是嗯。让接口声明getType() 或类似函数,并实现该接口。现在你有了自己对“类型”的定义,你可以塑造成任何你喜欢的东西......并且所涉及的类不需要相互关联。
猜你喜欢
  • 1970-01-01
  • 2012-07-28
  • 2020-09-30
  • 2014-01-09
  • 2016-08-27
  • 1970-01-01
  • 2019-10-06
  • 2016-05-29
  • 2014-02-17
相关资源
最近更新 更多