组成和继承。
我知道它们都是适当时选择的工具,上下文在组合和继承之间进行选择时非常重要。然而,关于每个的适当背景的讨论通常有点模糊;这让我开始考虑传统OOP的独特方面是多么明显的继承和多态。
多态性允许人们同样指定“is-a”关系以及继承。特别是,从基类继承隐式地创建该类与其子类之间的多态关系。然而,尽管可以使用纯接口实现多态,但是继承通过同时传输实现细节使多态关系复杂化。通过这种方式,继承与纯多态性完全不同。
作为一种工具,继承通过简化实现重用 在简单的情况下 ,为程序员提供与多态(通过纯接口)不同的方式。但是,在大多数情况下,超类的实现细节与子类的要求略有冲突。这就是为什么我们有“覆盖”和“成员隐藏”的原因。在这些情况下,继承所提供的实现重用是通过在级联级别的代码中验证状态更改和执行路径的额外工作来购买的:子类的完整“扁平化”实现细节分布在多个类之间,这通常意味着多个文件,其中只有部分适用于相关的子类。在处理继承时,查看该层次结构是绝对必要的,因为如果不查看超类的代码,就无法知道未覆盖的详细信息会对您的状态进行整理或转移执行。
相比之下,独占使用组合可以保证您可以看到可以通过显式实例化对象修改哪些状态,这些对象的方法可由您自行调用。真正扁平化的实现仍然没有实现(实际上甚至不可取,因为结构化编程的好处是实现细节的封装和抽象)但是你仍然可以重用代码,而只需要在一个地方查看当代码行为不端时。
我的目标是在实践中测试这些想法,避免传统继承为基于纯接口的多态性和对象组合的组合,我想知道,
有什么对象组合和接口无法实现继承吗?
修改
在迄今为止的回复中,ewernli认为一种技术没有技术专长,但另一种技术没有;后来他提到了每种技术固有的不同模式和设计方法。这是有道理的。但是,这个建议让我通过询问是否专用组合和界面代替传统继承来禁止使用任何主要设计模式来改进我的问题?如果是这样,在我的情况下是不是有相同的模式?
答案 0 :(得分:23)
从技术上讲,通过委托也可以实现通过继承实现的一切。所以答案是“不”。
将继承转换为委派
假设我们使用继承实现了以下类:
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的本质。这种事情不能通过授权轻松完成。另一方面,这正是使继承难以使用的原因。它需要精神上的扭曲来理解多态分派发生的位置以及如果覆盖方法会产生什么影响。
有一些关于继承的已知缺陷以及它可能在设计中引入的脆弱性。特别是如果类层次结构发展。如果使用继承,hashCode
和equals
中的equality也可能存在一些问题。但另一方面,解决一些问题仍然是一种非常优雅的方式。
此外,即使继承可以用委托代替,你也可以争辩说它们仍然达到了不同的目的并相互补充 - 它们没有传达相同的意图纯技术等价。
(我的理论是,当有人开始做OO时,我们很想过度使用继承,因为它被认为是语言的特征。然后我们学习授权,即模式/方法,我们学会喜欢它。经过一段时间后,我们找到两者之间的平衡,并发展直觉,在哪种情况下哪一个更好。嗯,正如你所看到的,我仍然喜欢两者,两者都值得一些谨慎之前被介绍。)
一些文献
继承和委托是 增量的替代方法 定义和分享。它有 人们普遍认为是代表团 提供了更强大的模型。这个 论文证明有一个 “自然”的继承模式 捕获的所有属性 代表团。独立,确定 对能力的制约 授权捕获继承 证明。最后,一个新的框架 这完全抓住了两个代表团 和继承是概述,有些 这种混合物的后果 模型被探索。
最有趣的一个 - 和 同时最有问题的概念 面向对象的编程是 遗产。继承是常见的 被视为特征 区分面向对象 从其他现代编程 编程范例,但研究人员 很少就其含义和用法达成一致。 [...]
由于类的强耦合和不必要的类成员的增殖诱导 通过继承,使用组合和委托的建议已经变得司空见惯。 在文献中提出相应的重构可能会让人相信这一点 这种转变是一项直接的任务。 [...]
答案 1 :(得分:3)
当快速枪支程序员试图通过添加方法和扩展层次结构(而不是考虑自然层次结构)来解决问题时,组合不会像继承一样搞砸生活
组成不会导致怪异的钻石,导致维护团队烧夜油刮伤头部
继承是GOF设计模式中讨论的本质,如果程序员首先使用了Composition,那么这些模式就不一样了。
答案 2 :(得分:1)
考虑使用gui工具包。
编辑控件是一个窗口,它应该继承窗口的close / enable / paint函数 - 它不包含窗口。
然后一个富文本控件应该包含编辑控件保存/读取/剪切/粘贴功能,如果它只包含一个窗口和一个编辑控件,那将非常难以使用。
答案 3 :(得分: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);
不那么干净,也打破了库的惯例
现在我并不是说你无法用构图完成这个(实际上我的例子表明你很明显可以),它在所有情况下都不理想,并且可能导致破坏惯例和其他相当不具吸引力的解决方法。
答案 4 :(得分:-1)
是。它是运行时类型标识(RTTI)。