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