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

C++和Java多是计算机科学中最严重的错误。二者都受到了OOP创始人Alan Kay本人以及其余许多著名计算机科学家的严厉批评。然而,C++和Java为最臭名昭著的编程范式--现代OOP铺平了道路。程序员

它的普及是很是不幸的,它对现代经济形成了极大的破坏,形成了数万亿美圆至数万亿美圆的间接损失。成千上万人的生命因OOP而丧失。在过去的三十年里,没有一个行业不受潜伏的OO危机的影响,它就在咱们眼前展开。算法

为何OOP如此危险?让咱们找出答案。编程

想象一下,在一个美丽的周日下午,带着家人出去兜风。外面的天气很好,阳光明媚。大家全部人都进入车内,走的是已经开过一百万次的同一条高速公路。数组

然而此次却有些不同了--车子一直不受控制地加速,即便你松开油门踏板也是如此。刹车也不灵了,彷佛失去了动力。为了挽救局面,你铤而走险,拉起了紧急刹车。这样一来,在你的车撞上路边的路堤以前,就在路上留下了一个150英尺长的滑痕。安全

听起来像一场噩梦?然而这正是2007年9月让-布克特在驾驶丰田凯美瑞时发生的事情。这并非惟一的此类事件。这是众多与所谓的“意外加速”有关的事件之一。“意外加速”已困扰丰田汽车十多年,形成近百人死亡。汽车制造商很快就将矛头指向了“粘性踏板”、驾驶员失误,甚至地板垫等方面。然而,一些专家早就怀疑多是有问题的软件在做怪。并发

为了帮助解决这个问题,请来了美国宇航局的软件专家,结果一无所得。直到几年后,在调查Bookout事件的过程当中,另外一个软件专家团队才找到了真凶。他们花了近18个月的时间来研究丰田的代码,他们将丰田的代码库描述为“意大利面条代码”——程序员的行话,意思是混乱的代码。dom

软件专家已经演示了超过1000万种丰田软件致使意外加速的方法。最终,丰田被迫召回了900多万辆汽车,并支付了超过30亿美圆的和解费和罚款。编程语言

意大利面条代码有问题吗?

Photo by Andrea Piacquadio from Pexels

某些软件故障形成的100条生命是太多了,真正使人恐惧的是,丰田代码的问题不是惟一的。函数式编程

两架波音737 Max飞机坠毁,形成346人死亡,损失超过600亿美圆。这一切都是由于一个软件bug, 100%确定是意大利面条式代码形成的。函数

意大利面条式的代码困扰着世界上太多的代码库。飞机上的电脑,医疗设备,核电站运行的代码。

程序代码不是为机器编写的,而是为人类编写的。正如马丁·福勒(Martin Fowler)所说:“任何傻瓜均可以编写计算机能够理解的代码。好的程序员编写人类能够理解的代码。”

若是代码不能运行,那么它就是坏的。然而若是人们不能理解代码,那么它就会被破坏。很快就会。

咱们绕个弯子,说说人脑。人脑是世界上最强大的机器。然而,它也有本身的局限性。咱们的工做记忆是有限的,人脑一次只能思考5件事情。这就意味着,程序代码的编写要以不压垮人脑为前提。

意大利面条代码令人脑没法理解代码库。这具备深远的影响--不可能看到某些改变是否会破坏其余东西,对缺陷的详尽测试变得不可能。

是什么致使意大利面条代码?

Photo by Craig Adderley from Pexels

为何代码会随着时间的推移变成意大利面条代码?由于熵--宇宙中的一切最终都会变得无序、混乱。就像电缆最终会变得纠缠不清同样,咱们的代码最终也会变得纠缠不清。除非有足够的约束条件。

为何咱们要在道路上限速?是的,有些人总会讨厌它们,但它们能够防止咱们撞死人。为何咱们要在马路上设置标线?为了防止人们走错路,防止事故的发生。

相似的方法在编程时彻底有意义。这样的约束不该该让人类程序员去实施。它们应该由工具自动执行,或者最好由编程范式自己执行。

为何OOP是万恶之源?

Photo by NeONBRAND on Unsplash

咱们如何执行足够的约束以防止代码变成意大利面条?两个选择--手动,或者自动。手动方式容易出错,人总会出错。所以,自动执行这种约束是符合逻辑的。

不幸的是,OOP并非咱们一直在寻找的解决方案。它没有提供任何约束来帮助解决代码纠缠的问题。人们能够精通各类OOP的最佳实践,好比依赖注入、测试驱动开发、领域驱动设计等(确实有帮助)。然而,这些都不是编程范式自己所能强制执行的(并且也没有这样的工具能够强制执行最佳实践)。

内置的OOP功能都无助于防止意大利面条代码——封装只是将状态隐藏并分散在程序中,这只会让事情变得更糟。继承性增长了更多的混乱,OOP多态性再次让事情变得更加混乱——在运行时不知道程序到底要走什么执行路径是没有好处的,尤为是涉及到多级继承的时候。

OOP进一步加重了意大利面条代码的问题

缺少适当的约束(以防止代码变得混乱)不是OOP的惟一缺点。

在大多数面向对象的语言中,默认状况下全部内容都是经过引用共享的。实际上把一个程序变成了一个巨大的全局状态的blob,这与OOP的初衷直接冲突。OOP的创造者Alan Kay有生物学的背景,他有一个想法,就是想用一种相似生物细胞的方式来编写计算机程序的语言(Simula),他想让独立的程序(细胞)经过互相发送消息来进行交流。独立程序的状态毫不会与外界共享(封装)。

Alan Kay从未打算让“细胞”直接进入其余细胞的内部进行改变。然而,这正是现代OOP中所发生的事情,由于在现代OOP中,默认状况下,全部东西都是经过引用来共享的。这也意味着,回归变得不可避免。改变程序的一个部分每每会破坏其余地方的东西(这在其余编程范式,如函数式编程中就不那么常见了)。

咱们能够清楚地看到,现代OOP存在着根本性的缺陷。它是天天工做中会折磨你的“怪物”,并且它还会在晚上缠着你。

让咱们来谈谈可预测性

Photo by samsommer on Unsplash

意大利面代码是个大问题,面向对象的代码特别容易意大利化。

意大利面条代码使软件没法维护,然而这只是问题的一部分。咱们也但愿软件是可靠的。但这还不够,软件(或任何其余系统)被指望是可预测的。

任何系统的用户不管如何都应该有一样的可预测的体验。踩汽车油门踏板的结果老是汽车加速。按下刹车应该老是致使汽车减速。用计算机科学的行话来讲,咱们但愿汽车是肯定性的

汽车出现随机行为是很是不可取的,好比油门没法加速,或者刹车没法制动(丰田问题),即便这样的问题在万亿次中只出现一次。

然而大多数软件工程师的心态是“软件应该足够好,让咱们的客户继续使用”。咱们真的不能作得更好吗?固然,咱们能够,并且咱们应该作得更好!最好的开始是解决咱们方案的非肯定性

非肯定性101

在计算机科学中,非肯定性算法是相对于肯定性算法而言的,即便对于相同的输入,也能够在不一样的运行中表现出不一样的行为。

——维基百科关于非肯定性算法的文章

若是上面维基百科上关于非肯定性的引用你听起来不顺耳,那是由于非肯定性没有任何好处。咱们来看看一个简单调用函数的代码样本。

console.log( 'result', computea(2) );
console.log( 'result', computea(2) );
console.log( 'result', computea(2) );

// output:
// result 4
// result 4
// result 4

咱们不知道这个函数的做用,但彷佛在给定相同输入的状况下,这个函数老是返回相同的输出。如今,让咱们看一下另外一个示例,该示例调用另外一个函数 computeb

console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );

// output:
// result 4
// result 4
// result 4
// result 2    <=  not good

此次,函数为相同的输入返回了不一样的值。二者之间有什么区别?前者的函数老是在给定相同的输入的状况下产生相同的输出,就像数学中的函数同样。换句话说,函数是肯定性的。后一个函数可能会产生预期值,但这是不保证的。或者换句话说,这个函数是不肯定的。

是什么使函数具备肯定性或不肯定性?

  • 不依赖外部状态的函数是100%肯定性的。
  • 仅调用其余肯定性函数的函数是肯定性的。
function computea(x) {
  return x * x;
}

function computeb(x) {
  return Math.random() < 0.9
          ? x * x
          : x;
}

在上面的例子中,computea 是肯定性的,在给定相同输入的状况下,它老是会给出相同的输出。由于它的输出只取决于它的参数 x

另外一方面,computeb 是非肯定性的,由于它调用了另外一个非肯定性函数 Math.random()。咱们怎么知道Math.random()是非肯定性的?在内部,它依赖于系统时间(外部状态)来计算随机值。它也不接受任何参数--这是一个依赖于外部状态的函数的致命漏洞。

肯定性与可预测性有什么关系?肯定性的代码是可预测的代码,非肯定性代码是不可预测的代码。

从肯定性到非肯定性

咱们来看看一个加法函数:

function add(a, b) {
  return a + b;
};

咱们始终能够肯定,给定 (2, 2) 的输入,结果将始终等于 4。咱们怎么能这么确定呢?在大多数编程语言中,加法运算都是在硬件上实现的,换句话说,CPU负责计算的结果要始终保持不变。除非咱们处理的是浮点数的比较,(但这是另外一回事,与非肯定性问题无关)。如今,让咱们把重点放在整数上。硬件是很是可靠的,能够确定的是,加法的结果永远是正确的。

如今,让咱们将值 2 装箱:

const box = value => ({ value });

const two = box(2);
const twoPrime = box(2);

function add(a, b) {
  return a.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

到目前为止,函数是肯定性的!

如今,咱们对函数的主体进行一些小的更改:

function add(a, b) {
  a.value += b.value;
  return a.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' == 6
// 2 + 2' == 8

怎么了?忽然间,函数的结果再也不是可预测的了!它第一次工做正常,但在随后的每次运行中,它的结果开始变得愈来愈不可预测。它第一次运行得很好,但在随后的每一次运行中,它的结果开始变得愈来愈不可预测。换句话说,这个函数再也不是肯定性的。

为何它忽然变得不肯定了?该函数修改了其范围外的值,引发了反作用。

让咱们回顾一下

肯定性程序可确保 2 + 2 == 4,换句话说,给定输入 (2, 2),函数 add 始终应获得 4 的输出。无论你调用函数多少次,无论你是否并行调用函数,也无论函数外的世界是什么样子。

非肯定性程序正好相反,在大多数状况下,调用 add(2, 2) 将返回 4 。但偶尔,函数可能会返回三、5,甚至1004。在程序中,非肯定性是很是不可取的,但愿你如今能明白为何。

非肯定性代码的后果是什么?软件缺陷,也就是一般所说的 “bug”。错误使开发人员浪费了宝贵的调试时间,若是他们进入生产领域,会大大下降客户体验。

为了使咱们的程序更可靠,咱们应该首先解决非肯定性问题。

反作用

Photo by Igor Yemelianov on Unsplash

这给咱们带来了反作用的问题。

什么是反作用?若是你正在服用治疗头痛的药物,但这种药物让你恶心,那么恶心就是一种反作用。简单来讲,就是一些不理想的东西。

想象一下,你已经购买了一个计算器,你把它带回家,开始使用,而后忽然发现这不是一个简单的计算器。你给本身弄了个扭曲的计算器!您输入 10 * 11,它将输出 110,但它同时还向您大喊一百和十。这是反作用。接下来,输入 41+1,它会打印42,并注释“42,生命的意义”。还有反作用!你很困惑,而后开始和你的另外一半说你想要点披萨。计算器听到了对话,大声说“ok”,而后点了一份披萨。还有反作用!

让咱们回到加法函数:

function add(a, b) {
  a.value += b.value;
  return a.value;
}

是的,该函数执行了预期的操做,将 a 添加到 b。然而,它也引入了一个反作用,调用 a.value += b.value 致使对象 a 发生变化。函数参数 a 引用的是对象 2,所以是 2value 再也不等于 2。第一次调用后,其值变为 4,第二次调用后,其值为 6,依此类推。

纯度

在讨论了肯定性和反作用以后,咱们准备谈谈纯函数,纯函数是指既具备肯定性,又没有反作用的函数。

再一次,肯定性意味着可预测--在给定相同输入的状况下,函数老是返回相同的结果。而无反作用意味着该函数除了返回一个值以外,不会作任何其余事情,这样的函数才是纯粹的。

纯函数有什么好处?正如我已经说过的,它们是能够预测的。这使得它们很是容易测试,对纯函数进行推理很容易——不像OOP,不须要记住整个应用程序的状态。您只须要关心正在处理的当前函数。

纯函数能够很容易地组合(由于它们不会改变其做用域以外的任何东西)。纯函数很是适合并发,由于函数之间不共享任何状态。重构纯函数是一件很是有趣的事情——只需复制粘贴,不须要复杂的IDE工具。

简而言之,纯函数将欢乐带回到编程中。

面向对象编程的纯度如何?

为了举例说明,咱们来讨论一下OOP的两个功能:getter和setter。

getter的结果依赖于外部状态——对象状态。屡次调用getter可能会致使不一样的输出,这取决于系统的状态。这使得getter具备内在的不肯定性

如今说说setter,Setters的目的是改变对象的状态,这使得它们自己就具备反作用

这意味着OOP中的全部方法(也许除了静态方法)要么是非肯定性的,要么会引发反作用,二者都很差。所以,面向对象的程序设计毫不是纯粹的,它与纯粹彻底相反。

有一个银弹

可是咱们不多有人敢尝试。

Photo by Mohamed Nohassi on Unsplash

无知不是耻辱,而是不肯学习。

— Benjamin Franklin

在软件失败的阴霾世界中,仍有一线但愿,那将会解决大部分问题,即便不是全部问题。一个真正的银弹。但前提是你愿意学习和应用——大多数人都不肯意。

银弹的定义是什么?能够用来解决咱们全部问题的东西。数学是灵丹妙药吗?若是说有什么区别的话,那就是它几乎是一颗银弹。

咱们应该感谢成千上万的聪明的男人和女人,几千年来他们辛勤工做,为咱们提供数学。欧几里得,毕达哥拉斯,阿基米德,艾萨克·牛顿,莱昂哈德·欧拉,阿朗佐·丘奇,还有不少不少其余人。

若是不肯定性(即不可预测)的事物成为现代科学的支柱,你认为咱们的世界会走多远?可能不会太远,咱们会停留在中世纪。这在医学界确实发生过——在过去,没有严格的试验来证明某种特定治疗或药物的疗效。人们依靠医生的意见来治疗他们的健康问题(不幸的是,这在俄罗斯等国家仍然发生)。在过去,放血等无效的技术一直很流行。像砷这样不安全的物质被普遍使用。

不幸的是,今天的软件行业与过去的医药太类似了。它不是创建在坚实的基础上。相反,现代软件业大可能是创建在一个薄弱的风雨飘摇的基础上,称为面向对象的编程。若是人的生命直接依赖于软件,OOP早就消失了,就像放血和其余不安全的作法同样,被人遗忘了。

坚实的基础

Photo by Zoltan Tasi on Unsplash

有没有其余选择?在编程的世界里,咱们能不能有像数学同样可靠的东西?是的,能够!许多数学概念能够直接转化为编程,并为所谓的函数式编程奠基基础。

是什么让它如此稳健?它是基于数学,特别是Lambda微积分。

来作个比较,现代的OOP是基于什么呢?是的,真正的艾伦·凯是基于生物细胞的。然而,现代的Java/C# OOP是基于一组荒谬的思想,如类、继承和封装,它没有天才Alan Kay所发明的原始思想,剩下的只是一套创可贴,用来弥补其劣等思想的缺陷。

函数式编程呢?它的核心构建块是一个函数,在大多数状况下是一个纯函数,纯函数是肯定性的,这使它们可预测,这意味着由纯函数组成的程序将是可预测的。它们会永远没有bug吗?不,可是若是程序中有一个错误,它也是肯定的——相同的输入老是会出现相同的错误,这使得它更容易修复。

我怎么到这里了?

在过去,在过程/函数出现以前 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程序员把大部分时间花在修复bug上。FP程序员把大部分时间花在交付结果上。

归根结底,函数式编程是软件世界的数学。若是数学已经为现代科学打下了坚实的基础,那么它也能够以函数式编程的形式为咱们的软件打下坚实的基础。

采起行动,为时已晚

OOP是一个很是大且代价高昂的错误,让咱们最终都认可吧。

想到我坐的车运行着用OOP编写的软件,我就惧怕。知道带我和个人家人去度假的飞机使用面向对象的代码并无让我感到更安全。

如今是咱们你们最终采起行动的时候了。咱们都应该从一小步开始,认识到面向对象编程的危险,并开始努力学习函数式编程。这不是一个快速的过程,至少须要十年的时间,咱们大多数人才能实现转变。我相信,在不久的未来,那些一直使用OOP的人将会被视为 “恐龙”,就像今天的COBOL程序员同样,被淘汰。C ++和Java将会消亡, C#将死亡,TypeScript也将很快成为历史。

我但愿你今天就行动起来——若是你尚未开始学习函数式编程,就开始学习吧。成为真正的好手,并传播这个词。F#、ReasonML和Elixir都是入门的好选择。


巨大的软件革命已经开始。大家会加入,仍是会被甩在后面?

相关文章
相关标签/搜索