面向对象编程的兴衰

阅读数:10339 2019 年 8 月 2 日

面向对象编程(OOP)并没有消亡。但与过去相比,它确实没有那么普及了。在 90 年代时,有很多面向对象编程相关的教科书和计算机科学课程。它就是“流行趋势”。然而,随着时间的流逝,人们开始意识到,严格的面向对象方法会带来很多问题。这些问题往往会使代码更复杂、更难以理解且更难以测试。

在 90 年代时,有很多面向对象编程相关的教科书和计算机科学课程。它就是“流行趋势”,是计算机的下一个浪潮。如果没有以这种风格编程,那么就是一个糟糕的程序员,或者至少落后于潮流。

当时,计算机科学(CS)专业的学生学习 OOP 的方法非常严格和教条。不仅鼓励实践者以对象和类的形式来构建应用程序,甚至还要求他们从对象和类的角度来考虑问题空间,这种实践方法被称为“面向对象的分析和设计”。

然而,随着时间的流逝,人们开始意识到,严格的面向对象方法会带来很多问题。这些问题往往会使代码更复杂、更难以理解且更难以测试。

事实证明,OOP 更适合某些特定的问题领域。例如,OOP 仍然是构建用户界面(窗口和按钮)最自然的方式 。但是,试图将面向对象技术应用于关系数据库一直是一场灾难。

以下是我观察到的一些问题。

“鸭嘴兽”效应

现实世界并不总是能被整齐地划分成具有明确属性定义的类别。例如,假设我们创建了一个代表动物王国的类层次结构。该类层次结构中既包含爬行动物(冷血、有鳞片、产卵等等),又包含哺乳动物 (恒温、有毛、生育等等),还包含鸟类、两栖动物、无脊椎动物等等。

然而,对于鸭嘴兽,它似乎不属于我们上述定义的任何类别。我们要做什么呢?我们是创建一个全新的类别,还是重新考虑整个分类方案呢?就工作量和程序复杂性而言,这两种方法都会产生显著的成本。

深层次结构

我记得当我还在谷歌工作的时候,有一个 JavaScript 库 goog.ui,它可以用于创建基于 Web 的用户界面。以下是这个库中某个 UI 组件的继承层次结构:

复制代码
class ToolbarColorMenuButton
* inherits from ColorMenuButton
* inherits from MenuButton
* inherits from Button
* inherits from Control
* inherits from Component
* inherits from EventTarget
* inherits from Disposable
* inherits from Object

九层的类继承好多。然而,情况还会变得更糟糕。

在这些高层类中,有许多都被只与少数子类相关的方法和属性“污染”了。例如,“Control”类有 90 多个方法。它具有设置状态的方法,即使特定的子类是无状态的;它有添加和删除子元素的方法,即使控件包含子元素也没有任何意义。

造成这种复杂性的一个重要原因是,该库的作者试图通过将组件放到类层次结构的不同位置来组织组件的不同功能 ,例如,组件是按钮还是滑块,或者组件是否有颜色 。

但实际上,这些不同功能的组件彼此之间无关。咖啡杯是红色的,和它是用陶瓷制成的,基本上是独立的属性。将红色咖啡杯归入“红色物品”的类别,并不比将其归入“陶瓷制品”甚至“家居用品”的类别更正确。任何一个都是任意选择的,因为类别是精神和社会的结构。

在谷歌工作的最后几年里,为了替代 goog.ui,我创建了一个名为“Quantum Wiz”的新用户界面工具包。我们采用的规则之一(以典型的谷歌风格,写成方程式)是:

复制代码
组合 > 继承

简单地说,这句话的意思是:

“更倾向于使用组合(组合是,用更小的构建块来组装组件功能的能力)而不是继承来作为代码重用的手段。”

因此,举例来说,如果一个按钮具有颜色,我们将通过向常规的“按钮”对象中添加一个“颜色”属性或子对象的形式来实现, 而不是再创建一个新的“颜色按钮”类。

由于这一强制要求,新工具包的类层次结构相对较浅,如果我没记错的话,只有两三层。而且它更容易理解和使用,也更强大了。

对象并不真实

Buckminster Fuller 曾经说过:“there are no things”。他的意思是,事物之间的区别很大程度上是人类的偏见。

例如,我所坐的沙发是被分子力束缚在一起的原子集合。然而,这些原子也会受到房间内其他物体的影响,如地毯、茶几、甚至是房间内的空气。沙发本身由各种部件组成,如纺织物、木材、金属弹簧等,这些部件也受到分子力的约束。沙发是一个对象,还是很多个对象?也许只有一个对象 ,即整个宇宙。

由于人类的视觉和触觉在很大程度上仅响应表面属性,如颜色和纹理 。我们更倾向于基于表面来对世界进行分类。相反,想象一下,如果我们能够直接感知周围物体内的分子组成。我们可能会看到一个“铜”对象,代表房子里的所有布线和管道;一个“氮”对象,代表房子里的气体;一个“水”对象;一个“木头”对象等等。

Fuller 的观点是,我们将世界“解析”为离散的“事物”的能力是任意的,与其说是物理现实,不如说是人类心理的反映。

因为面向对象的继承是将事物组织成类,所以它不能很好地模拟现实世界;它只能很好地模拟人类思考现实世界的方式。

方法也不真实

我记得大约 20 年前,一位软件供应商的技术代表试图向我公司的工程人员解释 OOP。他试图论证面向对象是一种模拟现实世界的方式,并给出一个类似咖啡杯的例子。他说这个杯子可能有一个“drink()”方法。

我记得我对此反应强烈,我认为他完全是胡说八道。物理对象,除了构建它的特定目的之外,还有许多用途。我可以用咖啡杯作为镇纸或门挡;这是否意味着它应该有一个“holdDownPapers()”或“keepDoorOpen()”方法呢?我可以将它用作武器、玩具或艺术品。我甚至可以将杯子摔成碎片,或将它研磨成粉末,然后以创造性的方式使用残余物。

内部逻辑 vs 外部逻辑

严格 OOP 风格的一个原则是,永远不可能从外部变更对象的内部状态。任何变更对象状态的业务或应用程序逻辑都必须作为对象本身的方法实现。因此,举例来说,如果要删除文本框中的所有文本,就不能简单地进行如下设置:

复制代码
textField.value = ""; // 设置成空字符串

这将违反规则。相反,我们必须这样设置:

复制代码
textField.clear(); // 清空文本框的内容

对于简单的操作来说,这很好。但是它很容易被带偏,特别是当正在处理的对象之间具有复杂关系时。

有时候,如果算法存在于任何对象之外会更好。这确实是一个值得强调的问题:对于这个问题集,我们更关心名词还是动词呢?

下面是一个具体的例子:最近我开始研究编译器(编写编译器是我的一个爱好;作为一名游戏开发人员,我发明了许多脚本语言)。在过去,当我编写编译器时,会采用非常严格的 OOP 方法来设计内部数据结构。有各种表示抽象语法树、表达式图、类型等的类层次结构。

通常,编译器通过一系列逐步执行或“传递”来处理这些数据结构,每一步的输出将作为输入传递到下一步中。

在过去,我倾向于按照推荐的 OOP 风格,为每个操作中的每个对象设置一些逻辑。这样做的结果很糟糕,添加得操作步骤越多,对象就变得越复杂。

更糟糕的是,它还使得为这些对象编写单元测试变得更困难。这些复杂的对象在创建之前需要大量的基础设施。这意味着为了测试这个对象,我必须创建大量的脚手架来满足所有的前提条件。

因此,我的测试覆盖率往往很低,因为编写测试是一项累人的工作。

最近,我采取了不同的方法。在我最新的编译器中,所有这些内部数据结构都是“哑”的,这意味着它们所做的只是保存数据,而不再是其他的了。所有用于操作和转换对象的代码都在这些对象的外部实现。

这对代码组织结构有很大的好处。每个算法都集中在一个地方,而不是分散在一堆源文件中。当我想测试某个给定的编译器操作时,我可以轻松地创建一些示例对象并将其提供给该操作。因此,我可以更容易得编写测试了,这意味着我可以编写更多的测试来提升测试覆盖率。

关系数据库

前面我提到了,以面向对象的方式处理关系数据库是有问题的。对象关系映射(ORM)被一位评论家称为“越南的计算机科学”。(注意:那篇文章很长,深奥难懂,而且有一定的倾向性。)

我的总体感觉是,在处理大数据时,不应将记录视为“对象”。关系数据库非常强大,但它们提供的强大功能并不是“类似对象”的。我更倾向于认为关系数据流是一种流体,在里面,我们可以使用代数运算来分割、转换、组合数据。

函数式编程

在过去十年左右的时间里,人们越来越关注函数式编程(FP)了。与 OOP 一样,函数式编程不仅仅是一件事,而是一组风格原则的集合。然而,它的要点是,OOP 侧重于与对象的交互或通信,而 FP 则侧重于对象的转换。通过“转换”,我的意思是获取某个对象,并将它传递给一个函数,结果将是一个全新的对象,这代表对输入进行了某些转换。原始对象要么被保留,要么被丢弃,但它不会以任何方式被修改或变更。

在我自己的编程过程中,我更喜欢使用“混合”的方式,在某些地方使用 FP 技术,而在其他地方使用 OOP 技术。 对于某些类型的问题来说,使用 FP 会事半功倍,而另一些问题,使用 OOP 则是明智的选择。

我知道很多 FP 爱好者都是“纯”函数式语言的狂热拥护者,在纯式函数语言中,所有对象都是不可变的,并且只能被转换,而不能被修改。然而,我发现纯方法往往会将一些相对简单的编程实践变成了谜题,我的意思是一些聪明而非显而易见的技巧,它们对喜欢脑筋急转弯的人很有吸引力,也很有趣,但是对其他人来说完全是不可理解的。

因此,我更倾向于在我认为有意义的地方使用 FP,并且以普通程序员阅读我的代码就能理解的方式。如果我想做一些聪明的事情,我会写一篇长篇评论来解释我所做的事情以及它是如何工作的(这满足了我的炫耀需求,我一直认为编程应该是一门表演艺术。)

总结

因此,面向对象编程已经今非昔比了。它仍然是一个很好的工具,值得学习。但它已经从基座上被拆除了,你很难看到有人像 25 年前那样以宗教般的狂热来吹捧它了。

原文链接:
https://medium.com/machine-words/the-rise-and-fall-of-object-oriented-programming-d67078f970e2

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论

最新评论

GEEKBANG_4696850 2019 年 08 月 12 日 09:28 0 回复
这个例子就写的不对,怕不是作者对分析的方法论存在什么误解。否则怎么可能得到杯子有drink谓语的结论。杯子作为容器只应该包含杯子自己的属性和状态,最多增加一个包含物的ref,drink是人的上层业务过程,操作了杯子、水和人的嘴。 所以有一种说法叫做DDD本质就是把OO做对,实际上面为什么你的系统不适合微服务、不适合敏捷等等,本质就是你压根没做对OO。然后你却得出结论,OO不好用、不正确。这是无能的基本标志。
Geek_e7b558 2019 年 08 月 07 日 20:12 0 回复
非常反感绝对化地讨论问题。OOP的核心思想之一就是模块划分,之上是对象划分。OOP的思想绝对不会主张把drink方法加入到杯子对象上。模块或对象的划分是OOP开发的基本功,作者对OOP的核心基础思想都没有掌握并以此举例是可笑的。OOP的一切皆对象一切皆接口不是教条地要你只能使用继承的方法,那些设计模式什么的都学习理解了吗?要说OOP的问题,我认为是掌握难度比较大,没有多年的实践很难掌握。但这其实不是OOP的锅,是所有编程都面临的问题,如何解耦,如何划分模块或对象,如何易于维护,如何拥抱变化。
展开全部
Maxwell 2019 年 08 月 05 日 11:17 0 回复
C++的标准库设计的就很漂亮。像容器类vector map等以及字符串类string。它们显然是一个个class。但是使用起来又不局限于纯面向对象。算法、容器和迭代器完美的融合在一起。
baobaoisme 2019 年 08 月 02 日 12:36 5 回复
对于有些观点不敢苟同
没有更多了