当前位置: 首页 > 科技观察

面向对象编程是计算机科学最大的错误

时间:2023-03-20 23:59:30 科技观察

C++和Java可能是计算机科学最严重的错误。OOP的创始人AlanKay和许多其他著名的计算机科学家都对这两者提出了严厉的批评。然而,C++和Java为最臭名昭著的编程范式——现代OOP铺平了道路。它的流行非常不幸,它对现代经济造成了严重破坏,造成了数万亿美元的附带损害。OOP已经杀死了成千上万的人。在过去的三十年里,没有任何行业受到潜在的OO危机的影响。为什么OOP如此危险?让我们找出来。想象一下,在一个美丽的周日下午带家人去兜风。外面很好,阳光明媚。所有人都上了车,在完全相同的高速公路上行驶了一百万次。但这一次不同——即使在你松开油门后,汽车仍会继续不受控制地加速。刹车也不灵了,好像没电了。为了挽救局面,你猛踩紧急刹车。在您的汽车撞到路边的路堤之前,这会在路上留下150英尺的防滑标记。听起来像一场噩梦?但这正是JeanBookout在2007年9月驾驶丰田凯美瑞时所发生的事情。这不是唯一的此类事件。这是困扰丰田汽车十多年并导致近100人死亡的与所谓的“意外加速”相关的众多事件之一。汽车制造商很快将矛头指向“粘踏板”、驾驶员失误甚至脚垫。然而,一些专家长期以来一直怀疑有问题的软件可能在起作用。为了解决这个问题,NASA的软件专家被免费征召入伍。仅仅几年后,在对Bookout事件的调查中,真凶被另一组软件专家发现。他们花了将近18个月的时间来编写丰田代码。他们将丰田代码库描述为“意大利面条代码”,这是程序员对混乱代码的术语。软件专家已经展示了丰田软件导致意外加速的超过1000万种方式。最终,丰田被迫召回超过900万辆汽车,并支付了超过30亿美元的和解金和罚款。意大利面条代码有问题吗?某些软件故障导致100条生命太多了。真正可怕的是,丰田代码的问题并不是独一无二的。两架波音737Max飞机坠毁,造成346人死亡,损失超过600亿美元。全部归因于软件错误,由100%确定的意大利面条代码引起。在全球范围内,意大利面条代码困扰着太多的代码库。机载计算机、医疗设备、在核电站上运行的代码。程序代码不是为机器编写的,而是为人类编写的。正如MartinFowler所说:“任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写的代码是人类可以理解的。”如果代码不运行,它就坏了。但是,如果人们不理解该代码,那么它就会被破坏。很快。让我们绕个弯,谈谈人脑。人脑是世界上最强大的机器。但是,它有其自身的局限性。我们的工作记忆是有限的,人脑一次只能思考5件事。这意味着程序代码不应以压倒人脑的方式编写。意大利面条代码使人脑无法理解代码库。这具有深远的影响——无法查看某些更改是否会破坏其他内容。对缺陷进行详尽的测试变得不可能。甚至没有人可以确定这样一个系统是否正常工作。如果它确实有效,为什么它甚至有效?是什么导致意大利面条代码?为什么代码会随着时间的推移变成意大利面条代码?因为熵——宇宙中的一切最终都变得混乱、混乱。就像电缆最终会变得一团糟一样,我们的代码最终也会变得一团糟。除非有足够的限制。为什么我们的道路有速度限制?是的,有些人会永远讨厌他们,但他们可以防止我们崩溃甚至死亡。路上为什么会有标志?防止人走错路,防止发生事故。在编程时,类似的方法将非常有意义。这样的约束不应该留给程序员来放置。它们应该由工具自动实现,或者理想情况下由编程范例本身实现。为什么OOP是万恶之源?>NeONBRAND在Unsplash上的照片我们如何实施足够的约束以防止代码变成意大利面条?两个选项-手动或自动。手动方法容易出错,而且人类总是会犯错误。因此,自动执行此类约束是合乎逻辑的。不幸的是,OOP不是我们一直在寻找的解决方案。它不提供任何约束来帮助解决代码纠缠。一个人可以精通各种OOP最佳实践,如依赖注入、测试驱动开发、领域驱动设计等(真的很有帮助)。然而,这些都不是由编程范式本身强制执行的(并且不存在用于执行最佳实践的此类工具)。没有一个内置的OOP特性有助于防止面条式代码——封装只是隐藏和分散程序中的状态,这只会让事情变得更糟。继承增加了更多的混乱。OOP多态性再次使事情变得更加混乱——不知道程序在运行时将采用哪条确切的执行路径没有任何好处。特别是当涉及到多级继承时。OOP进一步加剧了面条式代码问题缺乏适当的约束(以防止代码变得混乱)并不是OOP的唯一缺点。在大多数面向对象的语言中,默认情况下所有内容都是通过引用共享的。有效地将一个程序变成一个巨大的全球状态。这与OOP最初的思想是直接冲突的。OOP的创建者AlanKay具有生物学背景。他想到了一种可以用类似于生物细胞的方式编写计算机程序的语言(Simula)。他想让独立的程序(单元)通过相互发送消息来进行通信。独立程序的状态永远不会与外界共享(封装)。AlanKay从未打算让“细胞”直接进入其他细胞的内部进行改变。但这正是现代OOP中发生的事情,因为在现代OOP中,默认情况下所有内容都是通过引用共享的。这也意味着倒退成为必然。更改程序的一部分通常会破坏其他地方的功能(这在其他编程范例中很少见,例如函数式编程)。我们可以清楚地看到现代OOP从根本上是有缺陷的。每天在工作中折磨你的“怪物”。它也会在晚上困扰你。让我们谈谈可预测性>samsommer在Unsplash上的照片意大利面条代码是个大问题。面向对象的代码特别容易意大利化。意大利面条代码使软件无法维护。但这只是问题的一部分。我们还希望软件可靠。但这还不够,软件(或与此相关的任何其他系统)是可预测的。无论如何,任何系统的用户都应该有相同的可预测体验。踩下汽车的油门踏板总是会导致汽车加速。踩刹车总是会导致汽车减速。用计算机科学术语来说,我们希望汽车具有确定性。汽车表现出随机行为是非常不可取的,例如加速器无法加速或制动器无法制动(丰田问题)。即使这样的问题在万亿次中只发生一次。然而,大多数软件工程师的心态是“这个软件应该足够好,让我们的客户继续使用它”。我们真的可以做得更好吗?我们当然可以,而且应该这样做!最好的起点是解决我们计划的不确定性。非确定性101在计算机科学中,与确定性算法相反,非确定性算法是一种算法,即使对于相同的输入,每次运行的行为也可能不同。—关于非确定性算法的维基百科文章如果上面关于非确定性的维基百科声明对您来说听起来不好,那是因为非确定性没有任何好处。让我们看一个仅调用函数的代码示例:我们不知道该函数的作用,但它似乎总是在给定相同输入的情况下返回相同的输出。现在,让我们看另一个调用另一个函数computeb的示例:这一次,该函数为相同的输入返回不同的值。两者有什么区别?给定相同的输入,前一个函数总是产生相同的输出,就像数学中的函数一样。换句话说,函数是确定性的。后一种功能可能会产生预期值,但这并不能保证。换句话说,该函数是不确定的。什么使函数具有确定性或非确定性?不依赖于外部状态的函数是100%确定性的。只调用其他确定性函数的函数是确定性的。在上面的示例中,computea是确定性的,并且在给定相同输入的情况下始终会提供相同的输出。因为它的输出只取决于它的参数x。另一方面,computeb是非确定性的,因为它调用另一个非确定性函数Math.random()。我们怎么知道Math.random()是不确定的?在内部,它取决于系统时间(外部状态)来计算随机值。它也不需要任何参数——对于依赖于外部状态的函数来说,这是一个致命的赠品。决定论与可预测性有何关系?确定性代码是可预测的代码。非确定性代码是不可预测的代码。从决定论到非决定论>AnnieSpratt在Unsplash上的照片让我们看一下加法:我们始终可以确定,给定输入(2,2),结果将始终等于4。我们如何确定?在大多数编程语言中,加法运算是在硬件上实现的,换句话说,CPU负责计算结果始终保持不变。除非我们正在处理浮点数比较,否则(但那是另一回事,与非确定性问题无关)。现在,让我们关注整数。硬件是如此可靠以至于可以安全地假设加法的结果总是正确的。现在,让我们将值2框起来:到目前为止,功能正常!现在,我们对函数体做一些小改动:发生了什么?突然函数的结果不再是可预测的!第一次运行良好,但随着随后的每次运行,其结果开始变得越来越难以预测。换句话说,该函数不再是确定性的。为什么它突然未定义?该函数通过修改超出其范围的值而导致副作用。让我们回想一下确定性程序确保2+2==4。换句话说,给定输入(2,2),函数add应该总是得到4的输出。调用多少次都无关紧要函数,是否并行调用它,以及函数外部的外观。非确定性程序正好相反。在大多数情况下,调用add(2,2)将返回4。但有时,该函数可能会返回3、5甚至1004。不确定性在程序中是非常不受欢迎的,我希望您现在可以理解为什么。非确定性代码的后果是什么?软件错误或通常所说的错误。错误会花费开发人员宝贵的调试时间,如果将其投入生产,还会显着降低客户体验。为了使我们的程序更加可靠,首先要解决不确定性问题。副作用>IgorYemelianov在Unsplash上的照片这让我们想到了副作用的问题。什么是副作用?如果您服用治疗头痛的药物,而药物让您感到恶心,那么恶心就是一种副作用。简而言之,这是不可取的。假设您购买了一个计算器。你把它带回家,开始使用它,突然意识到这不是一个简单的计算器。你自己有一个扭曲的计算器!你输入10*11,它会输出110,但它也会对你大喊110。这是副作用。接下来,输入41+1,它会打印42,并带有注释“42,生命的意义”。一样的副作用!您感到困惑,并开始与想要订购比萨饼的另一半交谈。计算器偷听了谈话,大声说“是”,然后下了披萨订单。副作用也一样!让我们回到加法函数:是的,该函数做了它应该做的,将a加到b。然而,这也有副作用。调用a.value+=b.value会导致对象a发生变化。函数参数a引用了对象2,所以two.value不再等于2。第一次调用后,它的值变成4,第二次调用后,它的值变成6,以此类推。纯度>yannbervas在Unsplash上的照片在讨论了确定性和副作用之后,我们准备讨论纯度。纯函数是既确定又没有副作用的函数。同样,确定性意味着可预测——给定相同的输入,函数将始终返回相同的结果。没有副作用意味着该函数除了返回一个值外什么都不做。这样的功能是纯粹的。纯函数有什么好处?正如我已经说过的,它们是可以预测的。这使得它们非常容易测试(不需要模拟和存根)。关于纯函数的推理很容易——与OOP不同,不需要记住整个应用程序的状态。您只需要担心您当前正在使用的功能。纯函数可以很容易地组合(因为它们不会改变其作用域之外的任何东西)。纯函数非常适合并发,因为函数之间没有共享状态。重构纯函数非常有趣——只需复制和粘贴,不需要复杂的IDE工具。简而言之,纯函数将编程的乐趣带回。面向对象编程有多纯粹?为了说明这一点,让我们讨论OOP的两个特性:getter和setter。getter的结果取决于外部状态——对象状态。多次调用getter可能会导致不同的输出,具体取决于系统的状态。这使得吸气剂本质上是不确定的。现在,二传手。Setter用于更改对象的状态,使它们具有固有的副作用。这意味着OOP中的所有方法(静态方法除外)都是非确定性的,或者有副作用,但每个方法都不是一个好方法。所以面向对象编程不是纯粹的,它与纯粹恰恰相反。有一颗银弹。但是我们很少有人敢尝试。>照片由MohamedNohassi在Unsplash上拍摄无知并不过分是因为不愿学习。本杰明·富兰克林在充满软件故障的令人沮丧的世界中,有一线希望可以解决大多数(如果不是全部)问题。真正的银弹。但大多数情况下不会,前提是您愿意学习和应用。银弹的定义是什么?可以用来解决我们所有问题的东西。数学是万灵药吗?如果有的话,它几乎是灵丹妙药。我们要感谢数千年来为我们提供数学知识的数以千计的才华横溢的男男女女。欧几里得、毕达哥拉斯、阿基米德、艾萨克·牛顿、莱昂哈德·欧拉、阿朗佐·丘奇等等。如果不确定性(即不可预测性)成为现代科学的支柱,您认为我们的世界会走多远?可能不会很远,我们会留在中世纪。这在医学上确实发生过——在过去,并没有严格的试验来证实某种特定疗法或药物的疗效。人们依靠医生的意见来治疗他们的健康问题(不幸的是,在俄罗斯等国家仍然存在)。放血等无效技术在过去变得流行。像砷这样不安全的东西被广泛使用。不幸的是,今天的软件行业与过去的医学太相似了。它不是基于坚实的基础。相反,现代软件行业在很大程度上是建立在称为面向对象编程的脆弱基础之上的。如果人类的生命直接依赖于软件,面向对象的操作就会像放血和其他不安全的做法一样早已消失和遗忘。坚实的基础>ZoltanTasi在Unsplash上拍摄的照片还有其他选择吗?在编程的世界里,我们能不能有像数学一样扎实的东西呢?是的!许多数学概念直接转化为编程,并巩固了所谓的函数式编程的基础。根据。函数式编程是编程的数学——一个极其坚实和健壮的基础,可以从中构建可靠和健壮的程序。是什么让它如此强大?它基于数学,特别是Lambda微积分。相比之下,现代OOP基于什么?是的,正确的AlanKayOOP是基于生物细胞的。然而,现代Java/C#OOP是基于类、继承和封装等一组荒谬的思想,并没有AlanKay天才发明的原始思想。其余的只是一套创可贴,用来修补他们低劣心灵的缺点。函数式编程呢?它的核心构建块是一个函数,在大多数情况下是一个纯函数。纯函数是确定性的,这使得它们可以预测。这意味着由纯函数组成的程序将是可预测的。他们永远不会没有错误吗?不,但是如果程序中有错误,它也将是确定性的——相同的输入总是会发生相同的错误,因此更容易修复。我怎么到这里了?在过去,在过程/函数存在之前,Goto语句已广泛用于编程语言中。goto语句只是允许程序在执行期间跳转到代码的任何部分。这让开发人员很难回答“我如何到达执行点?”的问题。是的,这导致了很多错误。今天,一个非常相似的问题正在发生。只是这一次,难题是“我如何到达这个状态”,而不是“我如何到达这个执行点”。OOP(通常是命令式编程)回答了“我如何达到这种状态?”的问题。难的。在OOP中,一切都通过引用传递。从技术上讲,这意味着任何对象都可以被任何其他对象改变(OOP对此没有限制)。封装根本没有帮助——调用一个方法来改变一些对象字段并不比直接改变它更好。这意味着程序可以很快成为一堆依赖项,使整个程序成为全局状态的一大块。让我们不再问“我是如何达到这种状态”的解决方案是什么?您可能已经猜到了函数式编程。过去很多人拒绝停止使用goto的建议,就像今天很多人拒绝函数式编程和不可变状态的想法一样。但是等等,意大利面条代码呢?>图片来自Pexels的AndreaPiacquadio在OOP中,“优先继承胜过继承”被认为是最佳实践。从理论上讲,这样的最佳实践应该有助于意大利面条代码。不幸的是,这只是“最佳实践”。面向对象的编程范例本身对执行此类最佳实践没有任何限制。您团队中的初级开发人员必须遵循此类最佳实践并在代码审查中强制执行(并非总是如此)。函数式编程呢?在函数式编程中,函数组合(和分解)是构建程序的唯一方法。这意味着编程范式本身强制组合。正是我们一直在寻找的!函数调用其他函数,较大的函数总是由较小的函数组成。就是这样。与OOP不同,函数式编程中的组合是自然的。此外,这使得重构等过程变得非常简单——只需剪切代码,并将其粘贴到新函数中即可。无需管理复杂的对象依赖关系,也不需要Resharper等复杂的工具。可以清楚地看出,OOP是代码组织的次等选择。函数式编程的明显胜利。但是OOP和FP是互补的!很抱歉让你失望了。它们不是互补的。面向对象编程与函数式编程正好相反。说OOP和FP是互补的,就等于说放血和抗生素是互补的……是吗?OOP违反了许多基本的FP原则:FP促进纯度,而OOP促进杂质。FP代码基本上是确定性的,因此是可预测的。OOP代码本质上是不确定的,因此是不可预测的。组合在FP中是自然的,而不是在OOP中。OOP通常会导致错误的软件和意大利面条代码。FP产生可靠、可预测和可维护的软件。FP比简单的单元测试需要更少的调试。另一方面,OOP程序员生活在调试器中。OOP程序员将大部分时间花在修复错误上。FP程序员大部分时间都花在交付结果上。最终,函数式编程是软件世界的数学。如果说数学为现代科学提供了非常坚实的基础,那么它也可以以函数式编程的形式为我们的软件提供非常强大的基础。在为时已晚之前采取行动>图片来源:https://www.pexels.com/photo/blue-sky-161148/OOP是一个非常大且代价高昂的错误。让我们最终承认这一点。知道我所在的汽车正在运行用OOP编写的软件让我感到害怕。知道带我和家人去度假的飞机使用面向对象的代码并没有让我感到更安全。现在是我们采取最后行动的时候了。我们都应该开始采取小步骤来认识到面向对象编程的危险,并开始致力于学习函数式编程。这不是一个快速的过程,我们至少需要十年时间才能完成过渡。我相信在不久的将来,那些继续使用OOP的人将被视为“恐龙”,类似于今天COBOL程序员的过时方式。C++和Java将会消亡。C#会死。TypeScript也将很快成为历史。我希望你现在就采取行动——如果你还没有开始学习函数式编程。真正擅长它,并传播这个词。F#、ReasonML和Elixir都是入门的绝佳选择。大软件革命已经开始。你会加入还是落后?原文链接:https://suzdalnitski.medium.com/oop-will-make-you-suffer-846d072b4dce