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