首页
学习
活动
专区
工具
TVP
发布

面向对象的编程:一场万亿美元级的灾难

作者:Ilya Suzdalnitski是加拿大阿尔伯塔省Calgary公司的资深全栈工程师。

本文剖析了为什么是时候远离OOP了。

OOP被许多人认为是计算机科学界皇冠上的明珠,是编程部门的终极解决方案,可以消除我们的所有问题,是编写程序的唯一实际方式,是上天赐予我们的编程法宝......

实则不是,人们开始无法忍受抽象和随意共享的可变对象组成的复杂图形。宝贵的时间和脑力耗费在了思考“抽象”和“设计模式”上,而不是解决实际问题。

许多人批评面向对象编程(OOP),包括大名鼎鼎的软件工程师。就连OOP的发明者本人也猛烈炮轰现代OOP!

每个软件开发人员的终极目标应该是编写可靠的代码。如果代码有缺陷且不可靠,其他一切都不重要。编写可靠代码的最佳方法又是什么?简单性。简单性是复杂性的对立面。因此,我们软件开发人员的首要职责应该是降低代码复杂性。

免责声明

老实说,我不是面向对象编程的狂热粉丝。当然,这篇文章会有偏见。然而我不喜欢OOP有充分的理由。

我也明白抨击OOP是个很敏感的话题,可能会冒犯许多读者。然而,我认为我做的没错。我无意冒犯诸位,而是希望诸位对OOP带来的问题有更深刻的认识。

我无意批评Alan Kay发明的OOP,他是个天才。我希望OOP以他设计的方式来实施。我批评的是现代Java/C#对OOP采用的做法。

我认为,OOP被许多人(包括技术高管)视为编程部门事实上的标准是不对的。许多主流语言除了OOP外不为编程部门提供其他任何替代方案也是不可接受的。

老实说,我以前在开发OOP项目时痛苦不堪,也搞不清楚为什么这么痛苦。可能是我不够优秀?我那时想必须学习更多的设计模式!最终,我彻底被搞得筋疲力尽。

此文总结了我从面向对象编程向函数式编程转型的长达十年的历程。遗憾的是,无论我怎么努力,再也无法OOP的使用场景。我个人发现OOP项目很失败,因为它们变得太复杂而无法维护。

长话短说

面向对象程序是作为正确程序的替代方案而提供的......

——计算机科学界的先驱Edsger W. Dijkstra

面向对象编程在创建之初只想着一个目标:管理过程代码库的复杂性。换句话说,它应该改善代码组织。没有客观公开的证据表明OOP优于普通的过程编程。

令人痛苦的是,OOP偏偏处理不了本该处理的任务。乍一看它很好——我们有动物、狗和人等对象组成的干净的层次结构。然而,一旦应用程序的复杂性开始增加,它就束手无策。它不是降低复杂性,而是鼓励随意共享可变状态,因众多设计模式而带来了额外的复杂性。OOP使得重构和测试等常见的开发实践变得非常困难。

一些人可能不同意我的观点,但事实是,现代Java/C# OOP 从来没有正确设计过。它从来没有出现在一家正规的研究机构(与Haskell/FP相比)。Lambda演算为函数式编程提供了完整的理论基础。OOP在这方面根本比不了。

短期内使用OOP似乎没有害处,尤其是针对全新项目。但使用OOP的长期后果是什么? OOP好比是定时炸弹,将来代码库变得足够庞大时就会引爆。

项目延迟,未如期完工,开发人员精疲力竭,添加新功能几乎不可能。编程部门将代码库标为“遗留代码库”,开发团队计划重写代码。

OOP对于人脑来说不合自然,我们的思维过程以“做”事为中心:散步、与朋友交谈、吃披萨。我们的大脑已进化为做事,而不是将世界组织成抽象对象组成的复杂层次结构。

OOP代码是非确定性的——不像函数式编程,我们无法保证在给予同样输入的情况下获得同样的输出。这样一来,对程序作推论很困难。举个简单的例子,2+2或calculator.Add(2, 2)的输出基本上等于4,但有时可能等于3、5,甚至可能等于1004。Calculator对象的依赖项可能以微妙但深刻的方式改变计算的结果。真糟糕!

需要一种弹性框架

优秀的程序员编写优秀的代码,糟糕的程序员编写错误的代码,无论是什么编程范式。然而,编程范式应限制糟糕的程序员造成太大的破坏。当然,你不在其中,因为你已经在阅读本文,并努力学习。糟糕的程序员从来没有时间学习,他们只是在键盘上疯狂地按下随机按键。无论你喜不喜欢,都会与糟糕的程序员共事,其中一些人糟透了。而且,遗憾的是,OOP没有足够的约束机制来防止糟糕的程序员造成太大的破坏。真糟糕!

我不认为自己是糟糕的程序员,但如果没有一种强大的框架,我也编写不出优秀的代码。是的,一些框架与一些非常特殊的问题有关(比如Angular或ASP.Net)。

我不是要谈论软件框架。我谈论的是框架更为抽象的字典定义:“一种必不可少的支持结构”——与更抽象的东西(比如代码组织和处理代码复杂性)有关的框架。尽管面向对象编程和函数式编程都是编程范式,但它们也都是很高级的框架。

限制我们的选择

C++是一种糟糕的[面向对象]语言.....将你的项目限制于C意味着人们不会因任何愚蠢的“对象模型”把事情搞砸。

——Linux的开发者Linus Torvalds

Linus Torvalds因公开炮轰C++和OOP而广为人知。他完全正确的一点是,限制了程序员能做出的选择。实际上,程序员的选择越少,代码的弹性就越大。在上面那句引语中,Linus Torvalds强烈建议要有一种优秀的框架来构建我们的代码。

许多人不喜欢道路上的限速牌,但它们对于帮助防止人被撞车至关重要。同样,一种优秀的编程框架应提供阻止我们做蠢事的机制。

一种优秀的编程框架可以帮助我们编写可靠的代码。首先,它应通过提供以下几方面来帮助降低复杂性:

模块性和可重用性

适当的状态隔离

高信噪比

遗憾的是,OOP为开发人员提供了太多的工具和选择,又没有予以适当类别的限制。尽管OOP承诺可支持模块性并提高可重用性,但未能兑现承诺(稍后将详细介绍)。OOP代码鼓励使用共享的可变状态,这已再三被证明不安全。OOP通常需要大量的样板代码(低信噪比)。

函数式编程

到底什么是函数式编程?一些人认为它是一种高度复杂的编程范式,仅适用于学术界,不适合“现实世界”。事实并非如此!

是的,函数式编程有强大的数学基础,扎根于lambda演算。然而,它的大多数想法都是为应对更主流的编程语言中的弱点应运而生的。函数是函数式编程的核心抽象。如果使用得当,函数提供了一定程度的代码模块性和可重用性,这是OOP所从未见过的。它甚至还有解决为空性(nullability)问题的设计模式,提供了一种出色的错误处理方式。

函数式编程做得很好的一方面是,它帮助我们编写可靠的软件。几乎完全不需要调试器。是的,无需单步调试代码并观察变量。我本人已很久没碰调试器了。

最棒的方面是?如果你已经知道如何使用函数,那么早已是函数式程序员。你只要学习如何充分利用那些函数!

我不是在宣传函数式编程,也不关心你使用什么编程范式来编写代码。我只是想表明函数式编程提供的机制可以解决OOP /命令式编程所固有的问题。

我们都搞错了OOP

抱歉,我很久以前为这个话题首创了“对象”一词,因为它让许多人专注于这个次要的概念。重要的概念是消息传递。

——OOP的发明者Alan Kay

Erlang通常不被认为是面向对象的语言,但Erlang可能是唯一主流的面向对象语言。是的,Smalltalk当然是一种正宗的OOP语言——然而,它没有广泛使用。Smalltalk和Erlang都以发明者Alan Kay最初设想的方式使用OOP。

消息传递

Alan Kay在20世纪60年代首创了“面向对象编程”这个术语。他以生物学出身,因此试图使计算机程序如同活细胞那样进行联系。

Alan Kay的主要想法是通过向对方发送消息,从而让独立程序(细胞)进行联系。独立程序的状态永远不会与外界共享(封装)。

就是这样。OOP从来没打算要有继承、多态性、“新”关键字和无数设计模式之类的东西。

最纯粹的OOP

Erlang是最纯粹的OOP。与更主流的语言不同,它专注于OOP的核心思想:消息传递。在Erlang中,对象通过对象之间传递不可变消息进行联系。

有没有证据表明与方法调用相比,不可变消息是一种出色的方法?

当然有! Erlang可能是世界上最可靠的语言。它用于世界上大多数的电信以及互联网基础设施。用Erlang编写的一些系统的可靠性高达99.9999999%,没错九个9!

代码复杂性

使用OOP衍生而来的编程语言,计算机软件变得更冗长、可读性更低、描述性更差,更难修改和维护。

——Richard Mansfield

软件开发最重要的方面是降低代码复杂性。就是这样。如果代码库变得无法维护,任何花哨的功能都不重要。如果代码库变得太复杂、不可维护,即使100%的测试覆盖率也毫无价值。

什么让代码库变得复杂?有很多因素要考虑,但在我看来,主要因素是:共享可变状态、错误的抽象以及低信噪比(常常由样板代码引起)。所有这些在OOP中司空见惯。

状态的问题

什么是状态?简而言之,状态是存储在内存中的任何临时数据。想想OOP中的变量或字段/属性。命令式编程(包括OOP)从程序状态方面来描述计算和对该状态的更改。声明性(函数式)编程改而描述所需的结果,不明确指定对状态的更改。

可变状态——搞脑子的行为

我认为,你在构建可变对象组成的庞大对象图形时,大型面向对象程序面临越来越复杂的难题。你知道,要设法理解并记住你在调用方法时会发生什么、会有什么副作用。

——Clojure的发明者 Rich Hickey

状态本身无害。然而,可变状态是一大祸害,如果共享更是如此。可变状态究竟是什么?指可能会变的任何状态。想想OOP中的变量或字段。

请给出实际例子!

你有一张白纸,你在上面写下注释,最后得到不同状态(文字)的同一张纸。实际上,你已改变了那张纸的状态。

这在现实世界中完全没问题,因为可能没有其他人关心那张纸,除非这张纸是《蒙娜丽莎》原画。

人脑的局限性

为什么可变状态是个大问题?人脑是已知宇宙中功能最强大的机器。然而,人脑在处理状态时很糟糕,因为我们在工作记忆中每次只能容纳5样东西。如果你只考虑代码的用途,而不是它在代码库方面改变了什么变量,就一段代码作推论要容易得多。

用可变状态编程是一种搞脑子的行为。你我不知道,反正我只会同时耍两个球。要是给我三个或更多球,球肯定都会掉下来。那么为什么我们每天在工作中都要做这种搞脑子的事呢?

遗憾的是,可变状态所需的搞脑子正是OOP的核心。对象方法存在的唯一目的是改变这同一个对象。

零散的状态

OOP将状态分散在整个程序中,因此使代码组织更为糟糕。然后在各个对象之间随意共享零散的状态。

请给出实际例子!

暂且忘了我们都是成年人,假装我们在尝试拼装一辆很酷的乐高积极卡车。

然而有个问题——所有卡车零件与来自你其他乐高玩具的零件随机混合在一起。它们被再次随机地放入到50个不同的盒子中。还不允许你将卡车零件组合在一起——你得记住各个卡车零件的位置,还只能逐个取出零件。

是的,你最终会拼装好那辆卡车,但要花多长时间?

这与编程有什么关系?

在函数式编程中,状态通常是孤立的。你始终知道某个状态来自何处。状态永远不会分散在你的不同函数中。在OOP中,每个对象都有自己的状态;构建一个程序时,你得记住目前处理的所有对象的状态。

为了简化我们的生活,最好只有一小部分代码库处理状态。让应用程序的核心部分无状态且纯粹。这实际上是前端Flux模式(又名Redux)大获成功的主要原因。

随意共享的状态

好像嫌我们的生活因零散的可变状态还不够糟糕,OOP更进一步!

请给出实际例子!

现实世界中的可变状态几乎从来不是问题,因为事物是保密的,永不共享。这实际上是“适当的封装”。想象一位画家在绘下一幅蒙娜丽莎画。他在独自绘画,作最后的润饰,然后高价出售杰作。

现在,他对钱感到了厌倦,决定做有点不一样的事。他认为举办绘画派对是好主意。他请来朋友们:精灵、甘道夫、警察和僵尸来帮助自己。这是团队合作!他们都同时在同一块画布上开始绘画。当然,这画不出什么佳作——这幅画是彻头彻尾的灾难!

共享的可变状态在现实世界中毫无意义。然而,这正是出现在OOP程序中的一幕——状态在各个对象之间随意共享,而且以它们觉得合适的方式改变状态。反过来,随着代码库越来越大,这使得就程序作推论越来越难。

并发问题

OOP代码中随意共享可变状态使得这种代码的并行化几乎不可能。已经发明了复杂的机制来解决这个问题。已发明了线程锁定、互斥锁和其他许多机制。当然,这种复杂的方法有其自身的缺点:死锁、缺乏可组合性、调试多线程代码很困难很费时。我甚至没有谈到使用这种并发机制导致复杂性增加。

并非所有状态都是坏的

所有状态都是坏的?不,Alan Kay设想的状态可能不是坏的!如果是真正孤立的(不是“OOP那样”的孤立),状态变异可能是好的。

拥有不可变的数据传输对象也完全没问题。这里的关键是“不可变”。这种对象随后被用来在函数之间传递数据。

然而,这种对象也会使OOP方法和属性完全冗余。如果对象无法变异,拥有对象的方法和属性又有什么用?

可变性是OOP固有的

一些人可能认为,可变状态是OOP中的一种设计选择,而非义务。这种说法有个问题。它不是一种设计选择,几乎是唯一的选择。是的,你在Java/C#中可以将不可变对象传递给方法,但由于大多数开发人员默认数据突变,因此很少这么做。即使开发人员试图在OOP程序中使用不可变性,语言也没有为不可变性提供内置机制,也没有为有效处理不可变数据提供机制(即持久数据结构)。

是的,我们可以确保对象只通过传递不可变消息进行联系,永远不会传递任何引用(很少这么做)。这类程序会比主流OOP更可靠。然而一旦收到消息,对象仍然不得不改变自己的状态。消息是副作用,其唯一目的是引起更改。如果消息不能改变其他对象的状态,也就毫无用处。

不可能在不引起状态变异的情况下使用OOP。

封装的特洛伊木马

我们被告知封装是OOP的最大好处之一。封装理应保护对象的内部状态免受外部访问。但是这有一个小问题。它不管用。

封装好比是OOP的特洛伊木马。它通过使其看起来安全来推销共享可变状态这个想法。封装允许、甚至鼓励不安全的代码潜入代码库,从而使代码库从里面腐烂。

全局状态问题

我们被告知全局状态是万恶之源。应不惜一切代价避免全局状态。我们从未被告知的是,封装实际上是美化的全局状态。

为了使代码更高效,对象不是由其值传递的,而是由其引用传递的。“依赖项注入”(dependency injection)出问题就出在这里。

让我解释一下。每当我们用OOP创建一个对象,会将对其依赖项的引用传递给构造函数。那些依赖项也有自己的内部状态。刚创建的对象将那些依赖项的引用愉快地存储在内部状态中,然后愉快地以它高兴的任何方式修改。它还将那些引用一路传递给它可能最终使用的任何其他内容。

这会创建由随意共享的对象组成的复杂图形,这些对象最终改变彼此的状态。反过来,这会导致巨大的问题,因为几乎不可能看到什么导致程序状态改变。试图调试这种状态更改可能浪费好几天。如果你不必处理并发性,算你走运(稍后会详细介绍)。

方法/属性

提供对特定字段访问的方法或属性与直接更改字段的值一样差劲。是否通过使用花哨的属性或方法来改变对象的状态并不重要——结果都一样:变异状态。

现实世界建模存在的问题

有人说OOP试图为现实世界建模。事实根本不是这样——OOP在现实世界中与任何事物没有关系。试图将程序建模成对象可能是OOP的最大错误之一。

现实世界不是层次结构的

OOP尝试将一切建模成对象的层次结构。遗憾的是,现实世界中的事物不是这样子。现实世界中的对象使用消息彼此联系,但它们大多彼此独立。

现实世界中的继承

OOP继承并不是仿照现实世界。现实世界中的父对象无法在运行时改变子对象的行为。即使你从父母那里继承了DNA,他们也无法随心所欲地改变你的DNA。你不是从父母那里继承“行为”,你发展出自己的行为。你也无法“凌驾”你父母的行为。

现实世界没有方法

你写的那张纸有没有“写”方法?没有!你拿来一张空纸,拿起一支笔,然后写下一些文字。作为一个人,你也没有“写”方法——你根据外部事件或自身的内部想法来决定写一些文字。

名词的王国

对象以不可分割的单位将函数和数据结构绑定在一起。我认为这是根本性的错误,因为函数和数据结构属于全然不同的世界。

——Erlang的发明者Joe Armstrong

对象(或名词)是OOP的核心。OOP的一个基本限制是,它将一切强行当成名词。而并非一切都应建模成名词。不应将操作(函数)建模成对象。既然我们只需要乘以两个数的函数,为什么我们被迫创建一个Multiplier类?只需一个Multiply函数,让数据是数据,让函数是函数!

在非OOP语言中,执行将数据保存到文件之类的简易操作简单直观——酷似你用简单的英语来描述动作。

请给出实际例子!

当然,回到画家那个例子,画家拥有PaintingFactory。他请来了专门的BrushManager、ColorManager、CanvasManager和MonaLisaProvider。他的好朋友僵尸使用BrainConsumingStrategy。反过来,那些对象定义了下列方法:CreatePainting、FindBrush、PickColor、CallMonaLisa和ConsumeBrainz。

当然,这很简单,在现实世界中永远不会发生。为绘画这个简单的行为创造了多少不必要的复杂性?

如果允许函数与对象独立存在,不需要发明奇怪的概念来保?6?7?6?7存函数。

单元测试

自动化测试是开发过程的重要组成部分,可极大地帮助防止回归(即错误被引入到现有代码中)。单元测试在自动化测试过程中发挥了巨大的作用。

一些人可能不同意,但OOP代码极难进行单元测试。单元测试假设单独测试内容;而为了使方法可进行单元测试:

它的依赖项必须被提取到单独的类中。

为刚创建的类创建一个接口。

声明字段以保存刚创建的类的实例。

利用模拟框架来模拟依赖项。

利用依赖项入框架来注入依赖项。

仅仅使一段代码可测试,还要创建多少的复杂性?仅仅为了使一些代码可测试,浪费了多少时间?

PS:我们还不得不为整个类创建实例才能测试单单一个方法。这也将引入来自其所有父类的代码。

若使用OOP,为遗留代码编写测试尤为困难,几乎不可能。一些公司就是为解决测试传统OOP代码这个问题而创办的(比如TypeMock)。

样板代码

说到信噪比,样板代码可能是最大的祸害。样板代码是让程序可以编译所需的“噪音”。样板代码需要时间来编写,由于增加了噪音,使得代码库的可读性降低。

虽然“针对接口而非实现来编程”是OOP中推荐的方法,但并非一切都应成为接口。我们在整个代码库中不得不求助于使用接口,就为了确保可测试性。我们也可能不得不使用依赖项注入,这进一步带来了不必要的复杂性。

测试私有方法

有人说不应该测试私有方法......我往往不敢苟同,单元测试称为“单元”是有其原因的——单独测试小单元代码。然而,OOP中测试私有方法几乎不可能。我们不应纯粹为了可测试性而将私有方法作为internal。

为了实现私有方法的可测试性,通常需要将它们提取到单独的对象中。这反过来带来了不必要的复杂性和样板代码。

重构

重构是开发人员日常工作的重要组成部分。颇具讽刺意味的是,OOP代码很难重构。重构本应使代码更简单、更易于维护。恰恰相反,经过重构的OOP代码变得异常复杂——为了使代码可测试,我们必须使用依赖项注入,并为重构的类创建一个接口。即便如此,若没有像Resharper这样的专用工具,重构OOP代码其实很难。

在上面的简单示例中,仅仅为了提取一个方法,代码行数增加了一倍以上。既然当初重构代码是为了降低复杂性,为什么重构反而带来更大的复杂性?

将这与JavaScript中非OOP代码的类似重构进行对比:

代码实际上保持不变——我们只是将isValidInput函数移到一个不同的文件,并添加了一行以导入该函数。为了可测试性,我们还在函数签名中添加了_isValidInput。

这是个简单的例子,但随着代码库变大,实际上复杂性急剧增加。

而这并非全部。重构OOP代码极其危险。复杂的依赖项图和分散在整个OOP代码库中的状态使得人脑无法考虑所有潜在的问题。

权宜之计

某方法不管用时我们该怎么办?很简单,我们只有两个选择:抛弃它或尝试修复它。

OOP是不容易抛弃的东西,数百万开发人员受过OOP方面的培训。全球数百万组织在使用OOP。

你现在可能明白OOP其实不管用,它使我们的代码复杂且不可靠。不是只有你一个人这么认为!几十年来,人们一直在努力解决OOP代码中普遍存在的问题。他们提出了无数设计模式。

设计模式

OOP提供了一套指导原则,理论上应让开发人员可以逐步构建越来越大的系统:SOLID原则、依赖项注入、设计模式及其他原因。

遗憾的是,设计模式只不过是权宜之计。它们只是为了解决OOP的缺点而存在。这个主题方面的书有无数本。

问题工厂

实际上,不可能编写出优秀且易维护的面向对象代码。

一方面,我们有不一致的OOP代码库,似乎不遵守任何标准。另一方面,我们有一大堆过度设计的代码,一大堆错误的抽象(建立在另外的错误抽象上)。而设计模式对于构建这种塔状抽象大有帮助。

很快,添加新功能、甚至了解所有复杂性变得越来越难。代码库充斥着诸如此类的东西:

SimpleBeanFactoryAwareAspectInstanceFactory

AbstractInterceptorDrivenBeanDefinitionDecorator

TransactionAwarePersistenceManagerFactoryProxy

RequestProcessorFactoryFactory

试图理解开发人员自己构建的塔状抽象浪费了宝贵的脑力。在许多情况下,没有结构胜过拥有糟糕的结构(如果你问我的话)。

图片来源:

https://www.reddit.com/r/ProgrammerHumor/comments/418x95/theory_vs_reality/

OOP四大支柱的倒塌

OOP的四大支柱是:抽象、继承、封装和多态性。

让我们逐个看看它们到底是什么。

继承

我认为缺少可重用性的是面向对象语言,而不是函数式语言。因为面向对象语言的问题是它们拥有所有这种隐含的环境。你想要一个香蕉,但你拥有的是拿着香蕉的大猩猩和整片丛林。

——Erlang的发明者Joe Armstrong

OOP继承与现实世界无关。实际上,继承是实现代码可重用性的一种低劣方法。大名鼎鼎的四人帮(The gang of four)明确建议偏爱组合而非继承。一些现代编程语言完全避免继承。

继承存在几个问题:

带来了你的类甚至不需要的一旦代码(香蕉和丛林问题)。

在某个地方定义类的一部分使代码难以推论,多级继承的情况下更是如此。

在大多数编程语言中,多重继承甚至不可能。这基本上使继承这种代码共享机制毫无用处。

OOP多态性

多态性很棒,它让我们得以在运行时改变程序行为。然而它是计算机编程中一个很基本的概念。我不太确信为什么OOP如此关注多态性。OOP多态性可以完成工作,但它再次导致了搞脑子的行为。它使代码库变得异常复杂,并且对正在被调用的具体方法作出推论变得很困难。

另一方面,函数式编程让我们可以以极其优雅的方式获得同样的多态性,只要传入定义所需运行时行为的函数即可。有什么比这更简单的?无需在多个文件(和接口)中定义一堆重载的抽象虚拟方法。

封装

正如我们之前讨论的那样,封装好比是OOP的特洛伊木马。它实际上是美化的全局可变状态,使得不安全的代码看起来安全。不安全的编程做法是OOP程序员在日常工作中所依赖的支柱......

抽象

OOP中的抽象通过将不必要的细节隐藏起来不被程序员看见,以此解决复杂性。从理论上来讲,它应该让开发人员可以就代码库作推论,不必考虑隐藏的复杂性。

在过程/函数式语言中,我们只需将实现细节“隐藏”在一个相邻文件中。不需要将这个基本动作称为“抽象”。

为什么OOP主导行业?

Java可能是答案。

Java是自MS-DOS以来最让计算机界人士苦恼的技术。

——面向对象编程的发明者Alan Kay

Java很简单

Java于1995年初次推出时,与替代方案相比是一种很简单的编程语言。那时,编写桌面应用程序的准入门槛很高。开发桌面应用程序需要用C编写低级win32 API,开发人员也不得不关注手动内存管理。另一个替代方案是Visual Basic,但许多人可能不想被微软生态系统牢牢束缚。

Java被引入时,对许多开发人员来说是不二的选择,因为它免费,还可以在所有平台上使用。诸多功能使Java更易于上手,比如内置的垃圾收集、名称容易记住的API(相比晦涩难懂的win32 API)、命名空间和类似C的熟悉语法。

GUI编程也变得越来越流行,似乎各种UI组件很好地映射到类。IDE中的方法自动完成也使人们声称OOP API更易于使用。

如果不强迫开发人员使用OOP,Java可能不会那么糟糕。关于Java的其他一切似乎都很好。垃圾收集、可移植性、异常处理功能这些在1995年很棒,而其他主流编程语言缺乏这些功能。

随后C#问世

最初,微软一直高度依赖Java。开始出岔子、与Sun Microsystems就Java许可打起旷日持久的官司后,微软决定搞自家版本的Java。这时候C# 1.0问世。C#语言一直被认为是“更好的Java”。然而有个大问题:它是同一种OOP语言,同样的缺陷隐藏在稍加改进的语法下面。

微软一直在大力投资于其.NET生态系统,该生态系统还包括优秀的开发工具。多年来,Visual Studio可能是市面上最好的IDE之一。这反过来促使.NET框架得到广泛采用,特别在企业界。

最近,微软大力投资于浏览器生态系统,力推TypeScript。TypeScript很棒,因为它可以编译纯JavaScript,并添加静态类型检查等功能。不尽如人意的地方是,它对函数式结构未提供应有的支持——没有内置的不可变数据结构、没有函数组合、没有适当的模式匹配。 TypeScript是OOP优先的语言,基本上是面向浏览器的C#。C#和TypeScript的设计甚至都是Anders Hejlsberg一人负责的。

函数式语言

另一方面,函数式语言从未得到像微软这样大公司的支持。由于投入微不足道,F#无足轻重。函数式语言的发展主要由社区驱动。这也许可以解释OOP和函数式编程语言在受欢迎程度上的差异。

是时候翻篇了?

我们现在知道OOP是失败的试验。是时候翻篇了。我们社区是时候承认这个想法让我们失望了,我们必须放弃它。

——Lawrence Krubner

为什么我们坚持使用一种从根本上来说不尽如人意的组织程序的方式?这完全是无知吗?我对此表示怀疑,从事软件工程的人不笨。是我们使用诸如“设计模式”、“抽象”、“封装”、“多态性”和“接口隔离”之类的花哨OOP术语,从而在同行面前显得“看起来聪明”?恐怕不是。

我认为,我们很容易继续使用已经使用了几十年的东西。大多数人从未真正尝试过函数式编程。像我一样试过的人绝不回过头去编写OOP代码。

亨利•福特有过一句名言:“如果我问人们他们想要什么,他们会说更快的马。”在软件界,大多数人可能想要“更好的OOP语言”。人们很容易描述面临的问题(使代码库井井有条、不那么复杂),但那不是最好的解决办法。

有什么别的办法?

剧透:函数式编程。

如果说函子(functor)和单子(monad)之类的术语让你有点不安,并非只有你这样!如果为一些概念赋予更直观的名称,函数式编程就不会那么可怕了。函子其实是我们可以用函数来转换的东西,想想list.map。单子只是可?6?7?6?7以链接的计算!

试试函数式编程会使你成为更优秀的开发人员。你最终有时间编写解决实际问题的真实代码,不必花大部分时间来考虑抽象和设计模式。

你可能没有意识到自己已经是函数式程序员。你是否在日常工作中使用函数?是的话,就已经是函数式程序员!你只要学习如何最充分地利用那些函数。

Elixir和Elm是两种优秀的函数式语言,学习起来易于上手。它们让开发人员可以关注最重要的事情:编写可靠的软件,同时消除较传统的函数式语言存在的各种复杂性。

还有什么其他选择?贵公司已经在使用C#?不妨试试F#——这是一种了不起的函数式语言,可以与现有的.NET代码很好地协同操作。在使用Java?那么使用Scala或Clojure都是很好的选择。在使用JavaScript?借助正确的指导和代码检查(linting),JavaScript可以是一种很好的函数式语言。

OOP的捍卫者

我料到OOP的捍卫者会做出某种反应。他们会说此文充斥着不准确的地方。一些人甚至可能会开始骂娘。他们甚至可能称我是“菜鸟”开发人员,毫无实际的OOP经验。有人可能会说我的假设是错误的,例子也没用。

他们当然有权各抒己见。然而,他们辩护OOP的理由通常很弱。颇具讽刺意味的是,他们中大多数人可能从未用真正的函数式语言编过程。如果你从来没有真正试过两种语言,怎么能对两者进行比较?这种比较没多大用处。

迪米特法则(Law of Demeter)不是很有用——它无法解决不确定性问题,共享可变状态仍是共享可变状态,无论你如何访问或改变该状态。a.total()并不比a.getB().getC().total()好多少。它只是把问题隐藏了起来。

领域驱动设计(Domain-Driven Design)?这是一种有用的设计方法,它在复杂性方面有点帮助。然而,它还是无法解决共享可变状态这个根本的问题。

只是工具箱中的一个工具......

我常听到有人说OOP只是工具箱中的另一个工具。是的,它是工具箱中的一个工具,正如马匹和汽车都是交通工具......毕竟,它们都满足同一个目的,是吧?既然我们可以继续骑马,为什么开车?

历史在重演

这实际上让我想起了什么。20世纪初,汽车开始取代马匹。1900年,纽约只有几辆车在路上,人们一直用马来运输。人们抵制变革。他们称汽车是最终会消失的另一股“风潮”。毕竟,马匹已存在了好几个世纪!有人甚至要求政府介入。

两者有何关系?软件业以OOP为中心。数百万人接受了OOP方面的培训,数百万公司在其代码中使用OOP。当然,它们会竭力诋毁对他们的谋生之道构成威胁的任何东西!这只是常识。

我们清楚地看到历史在重演——20世纪上演的是马匹与汽车之争,21世纪上演的是面向对象编程与函数式编程之争。

云头条推荐视频~《面对对象编程很糟糕》:

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190823A0F74W00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券