组合是否有任何继承无法实现的功能?(涉及IT技术)

30

组合和继承。

我知道它们都是应选择适当时使用的工具,而在选择组合或继承时上下文非常重要。 然而,关于每种情况的适当上下文的讨论通常有点模糊;这让我开始考虑继承和多态性在传统面向对象编程中有多么不同。

多态性允许像继承一样平等地指定“是一个”的关系。 特别是,从基类继承隐式创建该类及其子类之间的多态关系。 但是,尽管可以使用纯接口来实现多态性,但继承通过同时传递实现细节而使多态关系复杂化。 这样,继承与纯多态性有着很明显的区别。

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

相比之下,独占使用组合可以保证您将看到哪些状态可以由显式实例化的对象修改,并且只会在需要调试代码时查看一个地方。 尽管并没有实现真正的扁平化,但这实际上也不是可取的,因为结构化编程的好处是封装和抽象实现细节,但您仍然可以重用代码,而且只需在一个地方查看代码即可解决代码问题。

在测试这些想法的过程中,放弃传统的继承,改用纯接口基础的多态性和对象组合的组合,我想知道,

有任何继承可以实现但是组合和接口不能实现的功能吗?

编辑

在迄今为止的回复中,ewernli认为没有一种技术可以做到另一种技术所不能做到的技术壮举;他随后提到如何不同的模式和设计方法是每种技术固有的。这是有道理的。然而,这个建议让我通过询问替代传统继承的组合和接口的独占使用是否会禁止使用任何主要设计模式来完善我的问题?如果是这样,那么在我的情况下,是否存在等效的模式可供使用?

3
就我个人而言,我喜欢使用混合(mixins)。 :) - Jonathan Feinberg
1
我没有时间检查有效的重复,但是继承与组合主题在SO上经常被讨论,例如:https://dev59.com/QnVC5IYBdhLWcg3wrDNd 或 https://dev59.com/4nI-5IYBdhLWcg3w8dYQ。`人们总是可以在这个主题上加以改变,并称其为对事物的新颖看法...` 然而,我们应该同意的一件事是,这种类型的问题并不能得出明确或权威的答案。也许CW可能更适合这种格式... - mjv
我并不想重复一场陈词滥调的辩论。诚然,我陈述我的观点的方式几乎是该辩论的单方面样本,但我的主要兴趣在于回答是否存在真正无法用组合和接口替代的继承用途。顺便问一下,什么是CW格式?也许我会尝试一下... - David V McKay
可能是是否真正需要继承?的重复。 - nawfal
继承通常被用作一种让最终用户通过重写方法来自定义某些行为的方式。组合并不直接取代继承中的这个方面,但是其他技术可以,比如策略模式。 - Scotty Jamison
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() ; ... }    
}

这段代码运行良好,调用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() ; ... }
}

我们基本上是重新实现了内置继承提供的功能。这有意义吗?不一定。但它说明了继承总是可以转换为委托。
讨论
继承的特点在于基类可以调用子类中被覆盖的方法。这是模板模式的本质。这些事情无法轻松地使用委托来完成。另一方面,这正是使继承难以使用的原因。需要进行心理转变才能理解多态分派发生的位置以及方法被覆盖时的影响。
关于继承和它可能引入的设计脆弱性,有一些已知的陷阱。特别是如果类层次结构发生变化。如果使用继承,还可能存在相等性hashCodeequals中的问题。但另一方面,它仍然是解决某些问题的非常优雅的方式。
此外,即使继承可以用委托替代,你可以争辩说它们仍然实现了不同的目的并相互补充 -- 它们没有传达相同的意图,这不是纯粹的技术等价所能捕捉到的。
我的理论是,当有人开始使用面向对象编程时,我们很容易过度使用继承,因为它被认为是该语言的一个特性。然后我们学习委托这种模式/方法,也开始喜欢它。经过一段时间,我们找到了两者之间的平衡,并发展出一种直觉,知道在哪种情况下哪种更好。嗯,正如你所看到的,我仍然喜欢两者,并且在引入它们之前都需要谨慎。
一些文献
- 委托就是继承 继承和委托是增量定义和共享的替代方法。通常认为委托提供了更强大的模型。本文证明了继承有一个“自然”的模型,可以捕捉所有委托的属性。此外,还证明了委托无法捕获继承的某些约束条件。最后,概述了一个完全捕获委托和继承的新框架,并探讨了这种混合模型的一些影响。
- 关于继承的概念

面向对象编程中最有趣且最棘手的概念之一是继承。继承通常被认为是区别于其他现代编程范式的特征,但研究人员很少就其含义和用法达成共识。[...]

由于类之间的强耦合和继承所引发的不必要类成员的增加,使用组合和委托的建议已经变得司空见惯。文献中对应的重构演示可能会让人误以为这样的转换是一个简单的任务。[...]


2
以下是我对这个主题的观点。接下来有三篇与该主题相关的论文引用,我引用了它们的摘要。DOI可以在链接中找到,所有链接都指向portal.acm.org。如果无法找到PDF文件,请告诉我。 - ewernli
2
有些东西迫使我点了踩。尽管我同意继承经常被误用,但问题/答案的基调让我感到不安。继承是一种专业工具。仅仅因为有人不知道如何使用它,就不能抛弃它。你也可以问:“Java能做什么,字节码不能做什么?”显然不能,但你自己也强调了这个问题:“这有意义吗?其实并没有。” - JoeG
我可能会爱上你的答案。 - Farhan stands with Palestine
你的例子很好,尽管不是最直接的,但你也简单地陈述了它可以克服,好像这很容易,但实际上并不那么容易。当涉及到多层次结构时,你提供了解决方案,但在日常工作中并不是那么直截了当。你提到了层次结构,试着用多级继承来做,其中继承将依赖于第四代祖先或“第四代孙子”上的第四代祖先,这样你就会有大量冗余和不透明的代码。 - FantomX1
1
在继承中,会自动检查子类中是否存在该方法,如果存在,则调用该方法,否则调用父类方法;在委托中,这种“多态性”不存在,当然,你可以更改子类(但不能更改父类),但你必须明确地定义是调用子类还是父类,或者使用一个帮助方法中介器,它仍然调用其中一个。在继承中,对于父类方法,您不必重复定义,只需为子类扩展即可;在委托中,您必须引用它。有点值得怀疑的是,一旦人们学会了如何惯常地做到这一点,如果经常使用继承,则继承通常更具优势。 - FantomX1
显示剩余2条评论

5

当快速枪手程序员试图通过添加方法和扩展继承关系(而不是考虑自然继承关系)来解决问题时,组合不会像继承那样搞砸生活。

组合不会导致奇怪的钻石问题,这会让维护团队加班挠头。

继承是GOF设计模式讨论的核心,如果程序员一开始就使用了组合,那么讨论将不会如此。


把我标记为巨魔吧,但在从中学习之前,我犯了所有这些错误。 - questzen
真的。我过去几天一直在修复完全依赖继承的代码。一个基类中必要的修复导致了1000行代码的更改。 - Nitin Nanda

0

我能想到只有一种情况,继承比组合更优。

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

不够简洁,也违反了库的惯例

现在我并不是说你不能通过组合来实现这个目标(事实上我的示例清楚地表明你确实可以),只是在某些情况下并不理想,并且可能会导致破坏惯例和其他视觉上不太吸引人的解决方法。


3
你强调了Widget是在一个外部、闭源库中。如果Widget.addChildWidget()方法接收Widget类型的参数,那么在我看来只有继承是可行的选择。该库追踪父控件下的子控件,但你的组合示例要求CustomWidgets跟踪它们的父控件,而不是被父控件追踪。(呕吐!)如果Widget.addChildWidget()方法接收IWidget类型的参数,那么组合仍然可以工作,因为CustomWidget可以实现IWidget,并将一些行为委托给一个私有的Widget。顺便说一句,接口组合的真正威力在于IOC和DI。 - David V McKay

0
考虑一个GUI工具包。
编辑控件是一个窗口,它应该继承窗口的关闭/启用/绘制功能 - 它不包含窗口。
然后,富文本控件应该包含编辑控件的保存/读取/剪切/粘贴功能,如果它仅包含窗口和编辑控件,使用起来将非常困难。

当讨论GUI类时,我认为很容易混淆视觉组合和类组合。可以创建IWindow来声明类似窗口的对象的签名,而Window则用于实际绘制窗口并处理事件。然后创建IEditControl和EditControl,它们实现了IWindow。EditControl简单地将其IWindow职责委托给Window对象。在公共方面,EditControl看起来像任何其他IWindow,并且像带有EditControl装饰的Window一样工作。最后,RichTextControl实现了IEditControl,将其委托给自己的EditControl对象。该模式是可链接的。 - David V McKay
2
真的,大多数继承的例子(如员工类等)最好使用组合来完成。但是GUI工具包确实受益于继承(在我看来)。 - Martin Beckett
4
敬启者:我的公司在 .Net 中维护着一套庞大的遗留系统;根据微软使用 WinForms 的示例,原始开发人员使用了大量继承来实现 UI 类。我们有18个独特(虽然类似)的组合框、12个基础表单以及一个高达8层的 CLI 继承层次结构。我们的UI框架非常混乱、不一致和脆弱,目前的开发团队甚至对最小的更改都感到惊恐。但是,通常,所有这些代码都不能满足新需求...使用组合意味着我们可以根据需要挑选功能,而不是无休止地派生新的组合。 - David V McKay

-1

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


如果你需要RTTI,坦白地说,你正在违反DIP原则。但是,无所谓了。声明一个getType()或类似的函数的接口,并实现该接口。现在你有了自己的“类型”定义,可以将其塑造成任何你喜欢的形式...而涉及的类不需要彼此相关联。 - cHao

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接