这是一个关于如何被迫放弃继承和面向对象编程的故事,作者仍然怀念它很长一段时间。为什么五年后的今天,作者还是认为继承在大多数情况下是一个较差的选择。
Rust 和 Go 等新兴语言非常强烈地反对继承,许多工程师似乎也同意这一点。这就是继承的终结吗?
大约五年前,我开始编写 Go。在我职业生涯的早期,我拥有多元化的背景,包括 Java、JavaScript、Python、C#、PHP 等。其中每一个都包含某种程度的面向对象支持,或者通过内置构造,或者通过缺乏类型。另一方面,Go 则不然。没有继承,没有抽象类,这些都没有。
经过前四年的广泛 Go 编码,即使在我公司的开发人员经验领先的情况下,我仍然主张继承。我最初会说继承比组合更好,然后我经历了承认它们都是达到目的的有效手段的阶段,但我个人更喜欢继承。最后,在编写 Go 近五年之后,“组合优于继承”震撼了我。
改变想法从来都不是一件容易的事,在各种帖子中公开表达自己的观点后更是如此。我想用有关两种编程范式的一些具体事实来支持它:继承和组合。
组合与继承并不是一个容易讨论的话题。我认为组合的结束和继承的开始并没有明确的界限。两者之间有很多灰色地带。
为什么我们需要组合与继承中的任何一个?
主要是为了避免代码重复。
代码重复会产生容易出错的代码库。
组合和继承是消除它的两种不同工具。
那么两者到底有什么区别呢?
继承是关于层次结构的,而组合是关于组合构建块的。
就像拼图游戏一样。
让我自己用最不准确和最具体的定义:
继承是关于“is-a”关系,而组合是关于“has-a”关系。
继承会声称狗是一种动物,而组合可能会认为狗具有某些动物特征。
让我们举一个示例问题来帮助我们完成整个讨论。我们有三种类型的动物,它们有一些共同的行为,但在其他方面有所不同:
在此示例中,Dog、Cat、 和Human包含非常明显的代码重复:
interface Animal { |
继承如何解决代码重复问题:
interface Animal { |
显然,继承对于这个问题有很多不同的解决方案。然而,如果这看起来很熟悉,那是因为这是一种相当典型的继承方法。
组合将如何解决这个问题:
interface Animal { |
由于组合不像继承那样结构化,因此它具有更多种方法。然而,这可能是最简单的组合解决方案。不同的组合解决方案会更复杂一些,但会消除更多的代码重复。它将利用函数组合:
interface Animal { |
由于继承和组合是两种完全不同的编程范例,因此它们创建了不同的代码结构。当你习惯看到其中一个时,另一个就显得很奇怪了。在它们之间转换意味着改变你的心态。尝试将两者混合可能会导致您不满意的结果。
banq:作者以上继承和组合代码是在没有考虑“动物”这个业务含义语义基础上的比较,如果抛开技术代码实现,只从领域语言本身概念上理解:
- 狗是一种动物吗?(is-a)
- 动物中有狗吗?(has-a)
这两种问法是不同的,主语不同,主语思维 决定了设计者的世界观
当前上下文是围绕什么主语展开的,围绕什么聚合核心展开的?
如果是围绕“狗”这个聚合核心,那么“狗”作为主语,选择第一种。
如果是围绕“动物”这个聚合核心,那么“动物”作为主语,选择第二种。
这种区别类似“分类 vs 分类中实体”,类似“集合 vs. 个体”,这又回到了罗素悖论 的理发师问题。
所以,继承和组合的选择从来不是由编程语言技术决定的,而是由业务领域语言决定的,但是由于很多人没有业务经验,只有用编程语言解决各种平台架构的经验,一叶遮目,觉得继承是多余的。
组合是否优于继承,这种选择也是由业务上下文决定的。
作者最后的总结
随着 Java 和 C# 等经典的面向对象语言显示出已经过了其巅峰期的迹象,而 Rust 和 Go 等语言在强烈反对继承的同时显示出持续增长的兴趣(来源),编程社区似乎已经受够了遗产。
虽然我强烈同意面向对象编程,特别是继承多年来被滥用,但我主要觉得完全否认继承有点幼稚,只不过是一种简单的趋势。
我仍然相信在对真实的基于分类的问题进行建模时,继承可以是一个很好的解决方案。
在其他所有情况下,我们可能应该避免它,即使生成的代码可能看起来更丑陋。
事实上,我被教条主义的信念蒙蔽了双眼,认为任何代码重复都是令人讨厌的。
(banq:复用代码从来不是设计的主要目的,GOF设计模式 中没有更多是为了节省代码,相反是增加了很多代码,)
在许多情况下,继承以保持代码简洁的名义降低了代码的可读性和可维护性。虽然我喜欢业务建模和编写继承,但理解和维护它可能会变得越来越糟。
作为提供实际业务价值的专业程序员,我们必须问自己哪个更重要:编写美观优雅的代码,还是编写其他人能够轻松理解和维护的代码。
原文点击标题
Reddit网友讨论
1、我的专业工作主要是 C++。我在高质量代码中看到的 95% 的继承只是接口/实现。Rust 和 Go 只是通过仅保留这种继承风格来简化继承模型。
2、我已经在 SE 工作了 10 多年(Java、C++,现在是 Rust)。我只在两种情况下看到继承:
- 多个具体类继承的接口,以允许类型擦除。
- 多个具体类继承的公共抽象类以获得一些共享功能。
OOP 教程总是呈现这些令人费解的示例,例如:“圆扩展了形状,但椭圆基本上是一个特殊的圆,因此椭圆扩展了圆,而球体基本上是......”。我从未见过这种做法在实践中发挥作用。
3、继承适用于以下情况:a)有很多繁琐的细节,b)大多数时候您只需要默认行为(有其他方法可以实现此目的,但它们通常很笨拙或不直观)。
图形界面需要继承:您通常有一两个自定义行为,但在大多数情况下,用户应该能够像平常一样单击、突出显示和复制文本等。如果您每次都必须实现所有这些,或者甚至只是列出您想要默认行为的所有内容,那么这会很烦人且耗时。
在我看来,最终没有一个万能的答案。
4、即使 James Gosling 也承认“类”继承对于 Java 来说是一个错误。
5、继承将不需要耦合的事物(数据、接口)耦合在一起。
不要认为这些语言是“反面向对象的”。相反,将它们视为建立在将应该独立的事物解耦的心态之上,并通过选择更好的抽象来做得更好。
6、添加泛型的继承是非常有趣的事情(提示:事实并非如此)。
如果做得正确(理解模式),它们的使用确实会带来巨大的代码可重用性(核心泛型会稍微牺牲可读性)
如果做得不当(花费大量时间) - 会增加巨大的复杂性,使代码更难重构等等。
在我看来,任何超过 2 层的内部/嵌套都会大大增加复杂性。
7、组合优于继承是来自面向对象编程的“四人帮”设计模式