“面向对象就是一个错误!”

IlyaSuzdalnitski Python大本营 昨天

作者 | Ilya Suzdalnitski

编译 | 弯月   责编 | 张文

出品 | CSDN(ID:CSDNnews)

C++和 Java 可能是计算机科学领域最大的错误。就连面向对象的创建者 Alan Kay 都曾对这两门语言提出了严厉的批评。然而,C++和 Java 都是比较主流的面向对象语言。

面向对象编程的流行是计算机科学领域的不幸,它对现代经济造成了极大的破坏,造成了数万亿美元的间接损失。在过去的三十年中,几乎所有行业都因潜在的面向对象编程危机而受到影响。

为什么面向对象编程如此危险?下面我们一起来寻找答案。

2007 年 9 月,美国 Jean Bookout 驾驶的 2005 款凯美瑞突然失控,Bookout 尝试刹车但是失败,最终发生了碰撞事故,导致车内另一人身亡,Bookout 受伤。然而,此案只是丰田在美上百起车辆意外加速投诉的其中之一。

在 Bookout 事件调查的过程中,原告方聘请了两位软件专家,他们花了将近 18 个月的时间来研究丰田代码。最终,他们都形容丰田代码库为“面条式代码”(Spaghetticode),程序的流向就像一盘面条一样扭曲纠结在一起。

软件专家演示了大量丰田软件可能导致意外加速的情况。最终,丰田被迫召回 900 多万辆汽车,赔付款项高达 30 多亿美元。

面条式代码有什么问题?

然而,丰田并不是唯一一家有面条式代码问题的公司。曾经有两架波音 737 Max 飞机坠毁,造成 346 人死亡,损失超过 600 亿美元。这两起事件的原因也出在了软件 bug 上,而且都是由面条式代码引起的。

面条式代码困扰着全世界上许许多多的代码库,包括飞机、医疗设备以及核电站上运行的代码。

程序代码不是为机器编写的,而是为人类编写的。Martin Fowler 曾说过:“任何傻瓜都可以编写计算机能够理解的代码。但只有优秀的程序员可以编写人类能够理解的代码。

如果代码不能正常运行,那说明出了问题。但是,如果人们不理解代码,那么它肯定会出问题。迟早的事儿。

此处,我们来谈论一下人类的大脑。人脑是世界上最强大的机器。但是,它有其自身的局限性。我们的工作记忆是有限的,人脑一次最多只能思考 5 件事。这意味着,程序代码的编写方式不应该超出人脑的局限。

然而,面条式代码导致人类无法理解代码库。这就会埋下深远的祸根,因为我们不清楚某些代码变动是否会引发问题。我们无法运行详尽的测试,找出所有缺陷,甚至没有人知道这样的系统是否能正常工作。即便系统能够正常工作,我们也不明白为什么。

面条式代码的起因

为什么经过一段时间的发展之后,代码库会出现面条式代码?因为熵。宇宙中的一切都变得混乱无序。就像电缆终将乱如一团麻,我们的代码最终也将变得混乱不堪。除非我们施加足够的约束。

为什么高速公路有时速限制?这是为了防止我们撞车。为什么道路上有交通信号?为了防止人们走错路,为了防止事故发生。

编程也一样。这样的约束不应让程序员来决定,应该通过工具自动实现,或者理想情况下通过编程范例本身来实现。

为什么面向对象是万恶之源?

我们怎样才能施加足够的约束,防止面条式代码的出现?两个办法:手动或自动。手动很容易出错,人类难免会犯错。因此,我们理应自动执行此类约束。

然而,面向对象编程并不是我们一直在寻找的解决方案。它没有提供任何约束来帮忙解决代码扭曲纠缠的问题。一个人可以精通各种面向对象编程的最佳实践,例如依赖注入、测试驱动的开发、领域驱动的设计等(这些实践确实有帮助)。但是,这些都不是由编程范例本身来强制执行的(而且也没有相应的工具来强制执行最佳实践)。

面向对象编程内部没有任何功能可以帮助我们预防面条式代码,封装只是隐藏和打乱了程序的状态,只会让情况变得更糟。继承带来了更多的混乱。面向对象编程的多态性更是火上浇油,我们根本不知道程序运行时会采用哪种确切的执行路径。特别是在涉及多个继承级别时。

面向对象进一步加剧了面条式代码的问题

然而,面向对象的缺点可不止缺乏适当的约束。

在大多数面向对象编程语言中,默认情况下一切都是通过引用共享的。这实际上将一个程序变成了一个庞大的全局状态。这与面向对象原本的思想背道而驰。面向对象的创建者 Alan Kay 拥有生物学的背景。他想到了一种语言(Simula),可以让我们按照生物细胞的组织方式编写计算机程序。他希望有独立的程序(细胞)通过相互发送消息进行通信。独立程序的状态永远不会与外界共享(封装)。

AlanKay 从来也没想过让“细胞”直接进入其他细胞的内部做任何修改。但现代面向对象编程就这么干了,因为在现代面向对象编程中,默认情况下,一切都是通过引用共享的。这也意味着破坏正常功能的错误无法避免。修改程序的某一部分就会破坏其他功能(这在函数式编程等其他编程范例中很少见。)

我们可以清楚地看到,现代面向对象编程本质上就存在很大的缺陷。它不仅会让你在日常工作中痛苦不堪,而且还会让你夜不成寐。

可预测性

面条式代码是一个重大的问题。面向对象的代码特别容易形成面条式。

面条式代码导致软件无法维护,但这只是问题的一部分。此外,我们还希望软件具有可靠性,以及可预测性。

任何系统的用户都应该享受相同的、可预测的体验。踩下油门,汽车就会加速;相反,踩刹车,汽车就会减速。用计算机科学术语来说,我们希望汽车的行为是确定的。

我们非常不希望汽车表现出随机行为,例如加速器无法加速,或制动器不能减速(丰田的问题)。即使此类问题发生的概率非常低。

然而,大多数软件工程师的心态都是“我们的软件要足够好,才能让客户继续使用。”我们能做的只有这么多吗?当然不是,我们应该做得更好!然而,首先最起码应该解决程序的不确定性。

不确定性

在计算机科学中,确定性算法指的是针对相同的输入,算法始终能够表现出相同的行为。而不确定性算法恰恰相反,即便输入相同,每次运行算法也会表现出不同的行为。

举个例子:

    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

      这一次,这个函数在面对相同的输入时,却给了不同的输出。这两个函数之间有什么区别?前者针对相同的输入,总是能给出相同的输出,就像数学函数一样。换句话说,这个函数是确定的。而后者则不一定会输出预期的值,换句话说,这个函数是不确定的。

      如何判断某个函数是确定的,还是不确定的?

      • 不依赖外部状态的函数百分百都是确定的。
      • 只调用其他确定的函数的函数也是确定的。
        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。无论这个函数被调用多少次,无论是否并行调用该函数,也无论函数外部是什么状况,它必然会输出 4。
              不确定的程序则恰好相反,在大多数情况下,add(2, 2)将返回 4。但有时,该函数可能会返回 3、5,甚至 1004。程序中万万不能出现不确定性,我希望你明白为什么。
              不确定的代码有什么后果?它们会引发软件缺陷,也就是常说的 bug。遇到 bug,开发人员需要浪费大量宝贵的时间来调试代码,如果将这类代码投入生产,则会大大降低客户体验。
              为了让我们的程序更可靠,首先应该解决不确定性的问题。

              副作用

              这里,我们不得不谈一谈副作用。
              什么是副作用?通常意义的副作用是指如果你因头痛而服用药物,但这种药物让你感到恶心,那么恶心就是一种副作用。简而言之,副作用就是不良反应。
              想象一下,你买了一台计算器。带回家后却发现这不是一台简单的计算器。你输入 10 * 11,而它却输出了 110,同时在你耳边大叫“一百一十”。这就是副作用。接着,你输入 41+ 1,它输出了 42,但又喊道“四十二,死掉。”这也是副作用。你带着满脸的疑惑,打电话叫外卖,结果这台计算器偷听到了你的电话,大声说“好的”,然后还帮你下了订单。这也是副作用。
              下面,我们继续来说 add 函数:
                function add(a, b) {  a.value += b.value;return a.value;}
                这个函数执行了预期的操作,即 a 加 b。但是,它也有副作用。因为在执行a.value+ = b.value 后,对象 a 会发生变化。假设刚开始的是 a.value=1,则第一次调用完成后,a.value=2。而且第二次调用后,它的值会再次变化。

                纯 粹

                在讨论了确定性和副作用后,我们再来看一看纯粹。纯函数是确定的,而且没有副作用。
                纯函数有什么优点?它们是可预测的。因此,非常易于测试(无需编写模拟函数和桩函数)。理解纯函数非常容易,不需要将整个应用程序状态都装入大脑。你只需要考虑眼前的函数。
                编写纯函数也很容易(因为它们不会修改任何范围之外的数据)。纯函数非常适合并发,因为函数之间没有共享状态。另外,重构纯函数也很简单,只需复制和粘贴,无需复杂的 IDE 工具。
                简而言之,纯函数可以让我们快乐地编程。

                面向对象编程是否是纯粹的?

                为了举例说明,我们看两个面向对象的特性:getter和setter。
                getter 的结果取决于外部状态,也就是对象状态。每次调用 getter,得到的结果都不相同,具体取决于系统的状态。因此,getter 本质上是不确定的。
                setter会修改对象的状态,因此它们本质上就带有副作用。
                这意味着面向对象所有的方法(除静态方法外)或者是不确定的,或者会带来副作用。因此,面向对象编程并不纯粹,它与纯粹背道而驰。

                银弹

                无知并不值得羞愧,无知又不学才让人羞愧。
                —— 本杰明·富兰克林
                虽然软件世界里困难重重,但我们仍存一线希望,即便无法解决所有的问题,至少也可以解决大多数问题。但是,只有当你愿意学习和应用,才能成功。
                银弹的定义是什么?就是可以解决所有问题的灵丹妙药。经过千百年的努力,数学界也有银弹。
                如果不确定性(即不可预测)成为现代科学的支柱,那你觉得我们的世界能走多远?可能不会太远,或许我们还停留在中世纪。医学界就经历过这样的情况,过去我们没有严格的试验来证实特定的治疗方法或药物的功效。
                不幸的是,如今的软件行业与过去的医学太相似了。现代软件行业的基础非常脆弱,也就是所谓的面向对象编程。我们希望软件也能够像医学一样,找到坚实的基础。

                坚实的基础

                在编程世界中,我们也可以拥有像数学一样可靠的基础吗?可以!我们可以将许多数学概念直接转化为编程,并以此为基础,也就是我们所说的函数式编程。
                函数式编程是编程界的数学,它是非常牢固且强大的基础,可用于构建可靠且健壮的程序。是什么让函数式编程如此强大?这种编程方式的基础是数学,尤其是 Lambda 演算。
                做个比较,现代面向对象编程的基础是什么?Alan Kay 的面向对象编程基于的是生物细胞。但是,现代 Java/C#的基础是一套荒谬的思想,比如类、继承和封装等,这些并非源自 Alan Kay 最初的思想。
                反观函数式编程,它的核心构件是函数,而且在大多数情况下是纯函数。纯函数是确定性的,因此它们是可预测的。这意味着由纯函数组成的程序将是可预测的。这倒不是说函数式编程没有 bug,但是如果程序中存在 bug,那也是确定的,即对于相同的输入始终会引发相同的 bug,因此非常容易修复。

                代码是如何执行到这一步的?


                以前,在过程式编程和函数式编程出现之前,goto 语句广泛用于编程语言中。goto 语句允许程序在执行的过程中跳至代码的任何部分。因此,开发人员很难回答:“代码是如何执行到这里的?”而且 goto 语句引发了大量 bug。
                如今,面向对象编程也有这个问题。在面向对象编程中,一切都是通过引用传递的。从理论上讲,这意味着任何对象都有可能被其他对象修改(面向对象编程对此没有任何约束)。封装根本没有帮助,它只不过是调用一种方法来更改某些对象的字段。这意味着,程序中的依赖关系很快就会乱成一锅粥,整个程序都会成为一个大型的全局状态。
                有什么办法可以解决这个问题吗?没错,就是采用函数式编程。
                过去曾经许多人对于停止使用 goto 的建议都有抵触,就像如今很多人会反对函数式编程和不可变状态的思想。

                面条式代码怎么办?

                在面向对象编程中,“组合优于继承”被视为最佳实践。从理论上讲,这类的最佳实践有助于改善面条式代码。然而,这只是“最佳实践”,面向对象的编程范例本身没有任何约束,强制人们遵守这类最佳实践。团队中的初级开发人员是否遵循这类最佳实践,完全看个人,或者你在代码审查中强制实施。
                函数式编程如何?在函数式编程中,函数组合(和分解)是构建程序的唯一方法。这意味着编程范例本身会强制执行组合。这正是我们一直在寻找的解决方案。
                函数调用其他函数,大函数始终由小函数组成。组合在函数式编程中是很自然的选择。此外,在这种方式下,重构的难度也会降低,只需剪切代码,然后将其粘贴到新函数中即可。无需管理复杂的对象依赖项,也不需要复杂的工具(例如Resharper)。
                我们可以看出,要想更好地组织代码,选择面向对象编程并不明智,函数式编程明显更胜一筹。

                现在就开始行动


                面向对象编程本身就是一个巨大的错误。
                如果我知道我乘坐的汽车运行的软件是由面向对象编程语言编写的,我会感到害怕;知道我和家人乘坐的飞机使用了面向对象的代码,也会让我感到不安。
                我们应该采取行动,认识到面向对象编程的危险,并努力学习函数式编程。我知道这个过程很漫长,至少需要十年才能做出转变。
                但我相信在不久的将来,终有一天面向对象编程会退出这个舞台,就像如今的 COBOL 一样。
                参考链接:https://suzdalnitski.medium.com/oop-will-make-you-suffer-846d072b4dce喜欢
                不看的原因确定内容质量低不看此公众号
                (0)

                相关推荐

                • C 程序员迈向百万年薪的最后一道坎

                  C++历史背景 C ++编程语言的历史可以追溯到1979年,当时Bjarne Stroustrup为博士学位论文进行了一些开发.在Stroustrup可以使用的所有单词中,有一种被称为Simula的语 ...

                • Go语言和Python哪一个更容易学呢?

                  作者:大众才子2019-11-07 15:24 Python和Go都是用于编写Web应用程序的强大的高级编程语言,它们之间有什么区别吗?下面本篇文章就来带大家认识一下Python和Go语言,介绍一下P ...

                • C与C++的真正区别在哪里(C语言通常被认为是面向系统的编程语言)

                  面向对象&面向过程 C++是C语言的延伸,可以理解为C++在C语言里加入了面向对象的特性.因为只有面向对象特性的加持,代码才能更好的复用.扩展和工程化,这是大型项目的必备要素. 而C语言是一种 ...

                • Python是什么?Python入门!

                  现如今,Python已经成为当下非常热门的编程语言,反超众多编程语言前辈,位居四大编程语言之一,可以应用在众多领域,因此越来越多的人都加入到Python学习大军,那么Python是什么?为什么Pyth ...

                • 稻盛和夫:忙碌一生,却又不成功的人,犯一个错误思维

                  世界上不存在100%有机的机会. 那些庸碌一生的人,往往都有一个错误的思维:他们一定要等都100%有利,万无一失,才肯去行动.世上没有完美的机会(骗局除外),这种习惯无疑是愚蠢的." --稻 ...

                • 尴尬了!马未都老师批评女书法博士后孙鹤的错误,也犯了一个错误

                  孙鹤老师因为在电视上讲颜真卿的<祭侄文稿>中的"刺史",一不小心成为一个众人皆知的人物. 孙鹤作为一个书法博士后,犯了个如此低级的错误,真是让书法爱好者无比汗颜,当然也 ...

                • 希望我今天的卖出不是一个错误!(5月6日操盘思考)

                  首先,祝大家五一小长假快乐,祝大家在外面玩的开开心心,少一些麻烦事! 另外,自驾游的时候,路上小心特斯拉的车辆,保持点车距较好! 涨停分析 行情不是很好,今天海龟社区就一只股票涨停,这只股票就是江苏吴 ...

                • 地方卫视节目过度娱乐化,是一个错误的导向,该整顿整顿了!

                  事先声明,我不太喜欢看湖南卫视的,个人认为它的节目对青少年毒害很深,所以,我家电视很少开,就应该让这样荼毒青少年的节目放在晚上十点以后播放,成年人爱看就看,黄金时段不应该放那样的节目. 说到底,我是支 ...

                • 夏天洗澡,很多人容易犯一个错误,洗错了,当心短寿!

                  随着夏天的来到,炎热的天气,很多人一天一把澡是少不了的.但洗澡还真不是一件简单的事,如果方法不对,不但容易致病,还可能会影响到我们的寿命,已经有人为此而丧命了,希望你重视.尤其是大热天!这个提醒真的太 ...

                • 我犯了一个错误,燃气板块买的偏了

                  集合竞价来看,市场很弱,9.25分的时候880005一点,上涨家数不到500家,有点意外,特朗普这个老滑头2000亿美元商品加税的问题就是不痛痛快快的打出来,这大盘就特么看他脸色一路下跌. 外围,蔚来 ...

                • 关于臀中肌常见的一个错误训练方式

                  臀中肌(Gluteus Medius),其神经支配源于L4.5S1的臀上神经.扇形,较厚,位于骨盆的外侧面,臀大肌的深面和臀小肌的浅面. (1)近端附着于:髂嵴前四分之三的髂骨外侧面,在臀前线和臀后线 ...

                • 不要因为一个错误而导致接二连三的错误

                  先问你四个问题: 1.你是否曾经强烈觉得市场要朝着某个方向运动?比如说觉得欧元兑美元会上涨超过100点. 2.接着你没有马上进场买进,而是发现欧元已经上涨了20点之后才买进? 3.你买进之后,欧元没有 ...

                • 发现好多人都犯了一个错误

                  周末有个新闻刷屏,一位阿姨2008年买了5万元的长春高新股票,后来出国去了忘了密码,最近回国来销户,一查账户市值已经500多万了. 我看了好几篇类似的报道,发现一件事:好多人都不知道股票长期收益率该怎 ...