你忘掉OOP的速度越快,你和你的软件就越好

也许这只是我的经验,但面向对象编程似乎是一种默认的,最常见的软件工程范例。

我花了好几年才打破它的咒语,并清楚地了解它是多么可怕和为什么。由于这种观点,我坚信人们必须了解OOP的错误,以及他们应该做些什么。

数据比代码更重要

所有软件的核心都是关于操纵数据以实现某个目标。目标决定了数据的结构,数据结构决定了必要的代码。

这部分非常重要,所以我会再说一遍。goal -> data architecture -> code。一定不要在这里改变顺序!在设计软件时,首先要弄清楚你想要实现什么,然后至少大致考虑数据架构:数据结构和有效实现它所需的基础架构。然后编写代码以便在这种架构中工作。如果目标发生变化,请更改架构,然后更改代码。

根据我的经验,OOP的最大问题是鼓励忽略数据模型架构并应用无意识的模式将所有内容存储在对象中,承诺一些模糊的好处。如果它看起来像一个Class的候选人,它就会进入一个Class。我有Customer吗?它进入了class Customer。我有渲染上下文吗?它进入了class RenderingContext。

开发人员的注意力不再是建立良好的数据架构,而是转向发明“好”类Class,它们之间的关系,分类法,继承层次结构等等。这不仅是一种无用的努力。这实际上是非常有害的。(banq注:数据类型比数据内容更重要,形式比内容更重要,形式逻辑减少人类的自身错误,不是所有东西必须实指到内容才有意义,罗素维根斯坦的贡献在于此)

复杂性

OOP程序往往只会增长而不会缩减,因为OOP鼓励它。而如果在明确设计数据架构时,目标通常是支持我们软件目标的最小可行数据结构集。(banq注:复杂性不是个数多,而是个数之间千丝万缕的关系复杂,个数复杂不可怕,可以通过增加人数对付,关系复杂,人数再多也没用,作者在这里没有认识到复杂性起源于关系复杂,而是数量多少)

图表无处不在

因为OOP需要在许多很小的封装对象中散布所有内容,所以对这些对象的引用数量也会爆炸。OOP需要在任何地方传递长参数列表或直接保存对相关对象的引用以使其快捷。(banq注:DDD聚合正是解决这些数量多的小对象)

你class Customer有引用order,class Order反之亦然。class OrderManager持有对所有的引用Orders,从而间接引用所有人Customer。一切都倾向于指向其他一切,因为随着时间的推移,代码中有越来越多的地方需要引用相关的对象。(banq注:作者这里意识到关系复杂,但是没有认识到通过分析需求找到聚合,找到整体与部分的关系,比如Order是一个聚合根,Customer也是聚合根,两者不能直接引用,可通过值对象嵌入彼此,通过异步事件更新同步)

你想要一个香蕉,但你得到的是一只拿着香蕉和整个丛林的大猩猩。

OOP项目看起来像是一个巨大的意大利面条图,而不是一个设计良好的数据存储,它们指向彼此的对象和采用长参数列表的方法。当您开始设计Context对象只是为了减少传递的参数数量时,您就知道您正在编写真正的OOP企业级软件。

(banq注:作者这里把OOA OOD和OOP混为一谈了)

跨领域的问题

一个简单的数据转换变成了一堆笨拙的,交织在一起的方法,除了封装的OOP教条之外,它们无缘无故地相互调用。在混合中添加一些继承为我们提供了一个很好的例子,说明了什么是陈规定型的“企业”软件。

对象封装是精神分裂症

我们来看看封装的定义:

封装是一种面向对象的编程概念,它将操作数据的数据和功能绑定在一起,并保护其免受外部干扰和误用。数据封装导致了重要的OOP数据隐藏概念。

实际上,对对象或类的粒度进行封装通常会导致代码试图将所有内容与其他内容(来自自身)分开。它产生了大量的样板:getters, setters, 多个构造器,奇怪的方法, 所有这些都试图防止不太可能发生的错误,其规模太小而无法实现。我给出的比喻是在你的左口袋上放一个挂锁,以确保你的右手不能从中取出任何东西。

不要误解我的意思 - 强制执行约束,特别是在ADT上通常是一个好主意。但是在OOP中,所有对象的相互引用,封装通常都没有实现任何有用的东西,并且很难解决跨越许多类的约束。

在我看来,类和对象太过细化,而专注于隔离,API等的正确方式是“模块”/“组件”/“库”边界。根据我的经验,OOP(Java / Scala)代码库通常是不使用模块/库的代码库。开发人员专注于为每个类设置边界,而不必过多考虑哪些类组合成一个独立,可重用,一致的逻辑单元。

有多种方法可以查看相同的数据

OOP需要一种不灵活的数据组织:将其分成许多逻辑对象,这些对象定义了一种数据体系结构:具有相关行为(方法)的对象图。但是,有多种逻辑表达数据操作的方法通常很有用。

如果程序中的数据以表格、面向数据的形式存储,则可以有两个或更多模块,每个模块在相同的数据结构上运行,但是以不同的方式运行。如果使用方法将数据拆分为对象,则不再可能。

这也是对象关系阻抗不匹配的主要原因。虽然关系数据架构可能并不总是最好的,但它通常足够灵活,能够使用不同的范例以多种不同的方式对数据进行操作。但是,OOP数据组织的严格性导致与任何其他数据体系结构不兼容。

表现不佳

在许多小对象之间分散的数据的组合,大量使用间接和指针以及首先缺乏正确的数据架构导致差的运行时性能。

该怎么办?

我认为没有银弹,所以我现在只描述它在我的代码中是如何工作的。

首先,数据考虑是第一位的。我分析了输入和输出,它们的格式,数量。如何在运行时存储数据,以及如何保持数据:必须支持哪些操作,速度(吞吐量,延迟)等等。

通常,对于具有任何大量数据,设计是接近数据库的。也就是说:会有一些像DataStoreAPI 这样的对象暴露出查询和存储数据所需的所有操作。数据本身将采用ADT / PoD结构的形式,数据记录之间的任何引用都将是ID(数字,uuid或确定性散列)的形式。在引擎盖下,它通常非常类似于或实际上由关系数据库支持:通过索引或ID存储大量数据的Vectr或HashMaps,以及快速查找等所需的“索引”的一些其他。其他数据结构如LRU高速缓存等也放在那里。

大部分实际程序逻辑引用这些DataStores,并对它们执行必要的操作。对于并发和多线程,我通常通过消息传递,actor模式粘合不同的逻辑组件。actor的例子:stdin阅读器,输入数据处理器,信任管理器,游戏状态等。这样的“actor”可以实现为线程池,管道元素等。如果需要,他们可以拥有自己的DataStore或与他人共享一个“演员”。

这样的架构给了我很好的测试点:DataStores可以通过多态实现多个实现,通过消息进行通信的actor可以单独实例化并通过消息的测试序列来驱动。

重点是:因为我的软件在具有如概念客户和订单的域中运行,但是并不意味着有任何Customer类,与其相关的方法。恰恰相反:Customer概念只是一个或多个表格中的一堆数据DataStore,“业务逻辑”代码直接操作数据。

软件工程中对OOP的批评并不是一件简单的事情。我可能无法明确阐述我的观点和/或说服你。如果你仍然感兴趣,这里有一些链接给你:

  • Brian Will的两个视频:面向对象的编程是糟糕的,面向对象的编程是垃圾:3800 SLOC示例
  • CppCon 2018:Stoyan Nikolov“OOP已死,面向数据的长期设计”,作者精美地介绍了一个示例OOP代码库并指出了它的问题。
  • 在wiki.c2.com上反对Oop的参数,以获取针对OOP的常见参数列表。
  • 面向对象编程是一项代价高昂的灾难,必须由劳伦斯·克鲁纳(Lawrence Krubner)结束 - 这篇文章很长并且深入到许多想法中

banq注:OOP与OOA/OOD是不一样的,总体来说OO是从便于人类的角度进行编程,适合以项目为主的工程,不一定适合产品级别。但是项目与产品的区别是:项目更加能够跟随需求变化,因为如果需求规范一旦不变,就如同水结成冰,走在冰上就很容易,如果你有微信这样级别的产品经理,尽管微信连一个简单首页清屏的功能都没有,照样是颠覆一群人的革命级别产品,这样强势的产品只有客户跟从你,但是更多情况下,我们做软件的是要跟从客户,客户的需求说变就变,而对象化方法背后其实是维根斯坦的逻辑概念与逻辑分析在支撑,是基本的世界观和方法论,模式也是可以使用数学来解释。当然这样从利于人的角度实现的代码不利于计算机执行,所以,在人和机器之间你得根据是项目还是产品进行倾斜。

你忘掉OOP的速度越快,你和你的软件就越好

相关推荐