为什么面向对象的编程是不好的编程范例。

好吧,让我们谈谈房间里的大象。 OOP在90年代初开始普及,那时候它是开发人员的圣杯。 但是现在,当项目变得越来越大时,它开始比解决常见问题更多地成为障碍。 我知道,没有什么是灵丹妙药,但是在函数式编程的出现(具有讽刺意味的是,它促进了许多OOP的良好实践),我们是否仍需要使用面向对象的编程作为选择的范式?

但是首先,让我们谈谈为什么经过这么多年OOP仍然如此受欢迎。

面向对象编程被Java所普及,Java是第一种广泛的面向对象语言。 为什么Java如此流行? 我认为这是几个方面的结合。 首先,它易于阅读,因为与其他语言相比,语法更简单,并且与英语相似(例如peter.love(angela) )。 其次,该程序在虚拟机上运行,​​因此开发人员无需考虑操作系统或CPU架构之类的问题。 第三,它不使用头文件,头文件的使用和维护使开发人员倍感头疼。 Java具有其特质,它通过降低入门级水平并使每个人的编码变得更加愉快,从而彻底改变了IT行业。 但是,那时的大多数项目都比当今的项目小得多且不那么复杂,并且随着时间的流逝,面向对象的方法变得越来越难以维护。 您可能会问:“但是这有什么问题呢?”。 我将答案分解为关键方面。

封装和依赖

OOP的规则之一是对象应像现实生活中的对象一样工作,例如小吃机(项目)包含许多子元素(对象),例如硬币槽,允许用户抛硬币(输入功能)并通过有关它的信息到键盘(异步输出信号)。 但是,大多数对象与现实生活中的对象并不相似,它们更加复杂并且具有许多依赖关系,从而使它们的封装方面无效。 在过去的很多次中,当我从事新功能的开发时,我有一个想法可以重用几年前工作过的另一个项目中的一些代码。 从OOP角度来看,一个好主意是仅从另一个项目中获取一个对象并在当前项目中使用它,因为根据面向对象的编程,对象应该是独立的封装子部分,我可以在任何地方重用我想要。 因此,我打开了一个先前的项目并进行了复制,但是,哦,不……该对象与其他两个对象有关系,因此我也复制了这两个对象。 但是,这些对象之一使用的自定义类型是在该旧项目的其他位置创建的。 这种情况可能会持续很长时间,并且由于存在依赖性问题,您可能被迫复制大量不需要的对象或大量代码。 在这种情况下,我只是说“操”,并编写了一个新的类似功能,该功能更适合新项目。 但是,我花在尝试重用理论上“可重用”的代码上的时间被永远浪费了,并且对当前项目没有任何价值。 当项目增长时,即使潜在的简单对象在依赖关系网中纠结在一起,也越来越有可能使它们变成一团糟,这使它们在项目外部完全无法使用。 乔·阿姆斯特朗(Joe Armstrong)将这个问题描述如下:

面向对象语言的问题在于,它们具有随身携带的所有隐式环境。 您想要香蕉,但是得到的是一只大猩猩,拿着香蕉和整个丛林。

乔·阿姆斯特朗

关系层次结构

这是与上一个类似的问题。 随着项目的发展,关系层次结构变得越来越大,越来越复杂。 这导致如下情况:

考虑这种情况。 对象J需要一些由对象G生成的数据,但它们都位于单独的“分支”上。 在这种情况下,开发人员应该怎么做? 有几种不同的解决方案来解决此问题。 从OOP角度来看,最好的解决方案是将请求通过对象I和C传递到最近的公共关节(在这种情况下为对象A),然后该“主”对象将通过对象B将请求发送给对象G。这个请求的请求也必须从对象G整个传递到J。如您所见,这是一场噩梦。 为了简化这种混乱,您可以做两件事。 您可以100%忠于OOP范例,并要求您的经理额外的时间来重构此功能,并查看他的脸会变得多红。或者,您可以进行简单的横切并直接连接这两个对象。 但是,从长远来看,这可能会导致许多问题。 最明显的现象之一是,创建跨领域连接会使代码更复杂,更难以阅读和维护。 但是,更危险的问题与核心对象特征之一有关。 对象包含两个元素组,内部状态(变量)和函数。 当两个对象要使用从属对象中的数据时(在这种情况下,这是直接相关的对象B和对象J通过交叉切割而相关联),则该对象可能开始产生意外结果。 为防止这种情况,开发人员必须花费更多时间在该对象内部创建某些功能,以防止其生成无效/意外/错误的结果。

经理,服务,工厂,助手等

大型项目的另一个问题是称为管理器,服务,助手等对象的数量在不断增长。它们的创建是为了封装一些功能,这些功能将用于多个不同的对象中。 如果将它们作为私有实体保留在这些对象中,则是一个好主意,因为它可以防止出现共享状态问题(在上一段中进行了介绍)。 但是,在大多数情况下,它们是在“主”对象的整个生命周期内存储的,即使“主”对象不使用它们也会增加内存使用。 更大的问题是管理者拥有一个共享实例,而这仅仅是一个单例。 这种方法是一个很大的错误,不应在任何地方使用。 是的,它解决了与广泛的层次结构有关的问题,但与此同时,很容易实现跨领域的交互,从而导致共享状态出现问题。 它还鼓励开发人员过度使用它,从而与管理器创建牢固绑定的“主”对象,并使它完全不可移植(类似于“封装和依赖关系”段落中描述的问题)。

基本/抽象类的漏洞

OOP的另一个非常普遍的问题是过度使用基类和/或抽象类。 继承和抽象类型是面向对象编程的基石,但是,它们有害无益,特别是在大型,复杂的项目中。 “基”类和抽象类都包含可以在子类中使用和覆盖的方法。 但是,超类的任何更改都可能影响其任何子级。 考虑以下示例:

多态性

多态是一个好主意。 当时可以重写超类中的方法并对其进行修改以适合它们的子类是一个巨大的创新。 但是现在,这就像建造一门大炮来发射蚊子。 您可以通过使用协议,扩展名或接口(取决于编程语言)来执行同一操作,从而完全避免使用此OOP元素。

遗产

继承是OOP的最亮点。 但是,您已经知道,上述某些问题与继承直接相关。 抽象类的漏洞使它们无法更改,多层继承使代码难以阅读和使用,这是过度工程和错误的体系结构决策的迹象。 当然,开发人员可以限制自己并尝试创建仅从“基”或抽象类继承的简单对象,这些对象可以提高代码的可读性,但不能解决“基/抽象类的漏洞”一节中描述的问题。 。 另外,当OOP允许他们这样做时,为什么开发人员应该限制自己呢? 难道不符合整个想法吗? 在我看来,类似于多态性,他们应该尽可能避免使用此OOP特性。 仅在必要时创建和使用的扩展,协议扩展对或小型功能对象之类的工具可以完全替代继承。

封装形式

当我第一次阅读封装时,我感到很惊讶。 具有简单,单一,封闭功能的小物件是一个绝妙的主意。 但是后来,我成为一名程序员,并开始与其他软件开发人员一起工作。 几周后,我发现封装理论只是一个神话。 要创建复杂的功能,必须创建很多小对象的地狱,这会导致维护整个项目的许多问题。 这些对象开始相互依赖,从而创建了由引用和自定义输入/输出类型组成的缠结网。 这种方法还提高了复杂性和可读性的水平,这使得与其他开发人员的协作更加困难,尤其是当他们刚刚进入项目时。 第二个问题是,传递对对象的引用可能会破坏它们的封装(我已经在上面的几段中讨论了共享状态问题)。

结论—怎么办?

OOP问题的核心是它是在很久以前定义的。 从那时起,IT项目变得越来越大,越来越复杂。 为了保持这种范例的相关性,开发人员开始创建修改和增强OOP的良好做法。 他们的努力为我们带来了诸如TDD,BDD,UML图,OOD,依赖注入,清晰的体系结构之类的东西,以及许多其他的东西,这些东西在理论上应该有助于在复杂且高度面向对象的项目中的工作。 每当我听到一个新的想法应该使OOP易于使用时,我都会微笑,因为这种情况类似于您可以在Internet上看到的有关共产主义/社会主义的有趣图片。

那么,如何解决与OOP相关的问题呢? 我认为我们需要革命而不是进化。 我们不能无限期地尝试解决这个破碎的范例。 相反,我们应该从不同的角度看待这些问题。 保持一切简单明了。 停止全局思考,不要试图预测所有事情,也不要设计整个体系结构。 您可能会问“为什么?”。 项目就是生命。 在开发过程中,可能有太多东西可以改变一切,因此,思考几个步骤只是浪费时间。 同样,尝试通过使用抽象或复杂结构使代码的各个部分通用也可能是死胡同,因为您可能再也不会使用这些“精采”的想法了。 因此,只需丢弃您的UML图和过度设计的想法(包括对象之间的多层,复杂的连接,膨胀的依赖关系等),并通过在DRY(不要重复自己)和KISS(保持)中进行思考,使代码简单易读简单,愚蠢)。 时不时地尝试以功能性方式实施某些事情,最重要的是,与您环境之外的其他开发人员进行大量交谈,并尝试理解他们的观点。

Interesting Posts