趁早放弃 OOP!

面向对象编程是个非常糟糕的想法,可能只能某些特定的地区适用,譬如加州。

——Edsger W. Dijkstra

也许只是我的个人经验,面向对象编程似乎成了大家的默认选择,成了软件工程最常见的范式。网上的各种编程教程不知为何都有意无意地使用了面向对象思想。

我知道面向对象的思想很难抗拒,而且表面上看这个思想非常伟大。我花了很多年才打破这个魔咒,才理解到它的糟糕之处和糟糕的原因。由于这一点,我强烈地相信,人们非常有必要理解 OOP 的问题,并弄明白有何替代办法。

接下来,我先谈谈我的个人看法。

数据比代码更重要

软件设计的核心思想是,所有软件的终极目的都是操纵数据以达到特定目标。目标决定了数据的结构,数据结构决定了代码的形式。

这一部分非常重要,所以我重复一遍。goal -> data -> architecture -> code。这个顺序不能变!在设计软件时,永远要先找到目标,然后至少要大概想一下数据的架构——用什么数据结构和基础设施才能有效地实现目标。然后才能在这种架构下开始编写代码。如果以后目标有变化,那就改变架构,再修改代码。

在我看来,OOP 的最大问题就是它鼓励忽略数据模型架构,而无脑添加一层,将所有东西都存储为对象,从而获得一些不知是不是好处的东西。如果某个东西看起来像个类,就把它写成个类。我有 Customer 吗?那就写成 class Customer。有渲染上下文吗?那就写成 class RenderingContext。

开发者不再关心如何建立好的数据架构,而是把注意力放到了发明“好”的类、类之间的关系、类的分类、继承层次结构等。这不仅毫无用处,实际上还非常有害。

鼓励复杂性

如果要明确地设计数据架构,那么通常能够产生一组能够支持软件目标的最小可行数据结构。而如果用抽象类和对象的方式来考虑,那么抽象层的复杂度和庞大就没有上限了。看看这个“FizzBuzz企业版”(https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition)就知道了。为什么如此简单的问题能写出这么多行代码?因为在 OOP 中永远都有进一步抽象的空间。

拥护 OOP 的人会认为这种现象跟开发者的技能水平相关。也许吧。但在实际中,OOP 程序只会越来越大,从不会缩小,因为这是 OOP 鼓励的方向。

香蕉猴子丛林问题

因为 OOP 要求把一切分散在许许多多非常小的封装对象中,这些对象间的引用数量也会爆炸式增长。OOP 到处都需要传递长长的参数列表,或者持有相关对象的直接引用。

class Customer 有个指向 class Order 的引用,反过来也一样。class OrderManager 持有指向所有 Order 的引用,因此间接地引用了 Customer。慢慢地,每个对象都会指向其他所有对象,因为随着时间的推移,越来越多的代码都会要求引用相关的对象。

你需要一个香蕉,但你得到的是一个拿着香蕉的猩猩,最后不得不得到整个丛林。

OOP 项目没有设计良好的数据结构,却有一大堆对象,互相引用的关系图和长长的方法参数列表看起来就像一堆意大利面条一样。哪一天你开始设计一个 Context 对象以便减少参数传递的个数时,你就知道你开始写真正的企业级软件的 OOP 了。

横切关注点

大部分重要的代码并不仅仅操作一个对象,它们实际上实现的是横切关注点(cross-cutting concerns)。例如:当 class Player hits() 一个 class Monster 时,我们应该修改哪个数据?Monster 的 hp 应该减小,减小量等于 Player 的 attackPower,如果 Monster 被杀死,那么 Player 的 xps 应该根据 Monster 的等级增加。这些动作应该在 Player.hits(Monster m) 中进行,还是应该在 Monster.isHitBy(Player p) 中进行?如果还有个 class Weapon 该怎么办?我们是该将它作为 isHitBy 的参数传递,还是 Player 应该有个 currentWeapon() 函数?

这个极其简单的例子只有三个互相影响的类,却已经成了一般 OOP 的噩梦。简单的数据变换变得非常难以实现,夹杂着各种方法的互相调用,一切只是因为 OOP 的封装。再添加一点继承,就成了“企业”软件中经常见到的样子。

对象封装是精神分裂

我们先来看看封装的定义:

封装是一个面向对象编程的概念,它将数据和操作数据的函数绑定在一起,避免外部对数据的干扰或滥用。数据封装是面向对象概念中的数据隐藏概念的核心。

本意是好的,但在实践中,在对象或类的粒度上封装,通常会导致代码把一切东西与其他东西分开。封装会产生大量的样板代码:getter,setter,多个构造函数,奇怪的方法,一切一切都只为了在一个小得没人关心的范围上防止不可能发生的错误。我想到的比喻就是,在你左边的口袋上装一个锁,以确保你的右手不能从里面拿东西。

不要误会——强制约束,特别是在抽象数据层上强制约束通常是正确的做法。但在 OOP 的大量对象互相引用中,封装通常不会起到任何作用,而且几乎不可能解决许多类的约束问题。

在我看来,类和对象过于细粒度,而真正应该关心的隔离、API 等应该是在模块、组件、库之间的边界上。据我的经验,OOP(Java / Scalar)的代码通常没有任何模块或组件的概念。开发者专注于在每个类上设置便捷,而不去考虑哪组类应该放在一起组成一个独立、可重用、一致的逻辑单元。

同一份数据有多种观察角度

OOP 要求一种不灵活的数据组织方式:将数据分割成许多逻辑对象,这些对象定义了数据架构——对象间的关系图以及关联的行为(即方法)。但是,从多种逻辑角度表示数据操作通常更有用。

例如,如果程序的数据存储成表格等面向数据的形式,那就可以有多个模块,这些模块利用不同的形式操作同样的数据结构。如果将数据分割成对象,这种方式就不可能了。

这也是对象和关系之间出现不匹配现象(Object-relational impedance mismatch,https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch)的原因。尽管关系型数据结构并不总是最好的,但它足够灵活,可以用多种方式操作数据,可以支持不同的范式。但是,OOP 顽固的数据组织形式无法与任何其他数据架构兼容。

糟糕的性能

数据分散在许多小型对象中,大量的间接引用和指针,缺乏正确的架构,都会降低运行时的性能。

那应该怎么做?

我不认为有银弹能解决这个问题,所以我还是说一下我现在写代码时的习惯做法。

第一条,首先要考虑数据。我会分析输入和输出,分析它们的格式和数据量,运行时数据应当如何存储,如何持久化,数据应当支持什么操作,各种操作的速度(吞吐量、延迟)等。

通常,数据量较大的数据设计都会与数据库相似。也就是说,应该有个类似于 DataStore 的对象,它的 API 应当支持所有查询和存储数据所需的操作。数据本身以 ADT(抽象数据结构)或 PoD(Plain Old Data,纯数据格式)的结构存储,任何数据记录之间的引用应当以ID(数字格式、uuid或确定性哈希)的形式存在。在底层,通常应当使用关系型数据库,或者使用类似于关系型数据库的结构:Vec 或 HashMap 用于根据 index 或 ID 保存大量数据,需要快速查找的数据要使用“索引”等。其他数据结构如 LRU 缓存等也可以用在这里。

大量实际的程序逻辑会引用这种 DataStore,并进行相应的操作。对于并发和多线程编程,我通常会将各个逻辑组件通过消息传递和角色方式连接到一起。角色的例子有:读取标准输入的程序,输入数据处理器,信任管理器,游戏状态,等等。这些“角色”可以用线程池实现、元素流水线等实现。在必要时,角色可以拥有自己的 DataStore,或者与其他“角色”共享。

这种架构可以支持非常好的测试点:通过多态,DataStore 可以有多种实现,而角色通过消息传递来通信,使得它们可以单独初始化,并用测试时的消息序列来驱动。

这里的重点是:因为我的软件用于某个特定的领域,该领域有许多诸如 Customer、Order 等概念,但这些概念并不意味着必须有个 Customer 类,类上有一些方法。实际情况正相反:Customer 概念只是一堆表格形式的数据,存储在一个或多个 DataStore 中,而“业务逻辑”直接操作这些数据。

参考阅读

跟软件工程中的许多其他概念一样,批评 OOP 并不是件容易的事情。上文中我有的些观点可能说得并不清楚,如果你感兴趣,可以参考以下内容,希望帮助大家更好地了解 OOP:

Brian Will 的两个视频,他阐述了许多反对OOP的观点:“可怕的面向对象编程”(https://www.youtube.com/watch?v=QM1iUe6IofM)和“面向对象编程是个垃圾:3800行代码的例子”(https://www.youtube.com/watch?v=V6VP-2aIcSc)

CppCon 2018:Stoyan Nikolov “OOP已死,面向数据设计万岁”(https://www.youtube.com/watch?v=yy8jQgmhbAU)这个视频的作者漂亮地用一个 OOP 代码的例子指出了问题所在。

wiki.c2.com上关于OOP的讨论:http://wiki.c2.com/?ArgumentsAgainstOop,剖析了使用 OOP 的常见观点。

Lawrence Krubner 的“面向对象编程是个昂贵的灾难,必须放弃”(http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end)这篇文章很长,深入分析了许多观点。

原文:https://dpc.pw/the-faster-you-unlearn-oop-the-better-for-you-and-your-software

作者:Dawid Ciężarkiewicz,开源软件开发者,软件工程师。波兰人,2013年来到硅谷,曾加盟过Nvidia、PacketZoom、Rubrik等公司。现在在BitGo工作,从事区块链相关的产品和基础设施开发。

译者:弯月,责编:屠敏

【End】

微信改版了,

想快速看到CSDN的热乎文章,

赶快把CSDN公众号设为星标吧,

打开公众号,点击“设为星标”就可以啦!

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181206A1LQYM00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券