欧洲科技圈 是由来自欧洲各国在海外学习、工作、生活多年的精英人才创立的架在欧洲人才与中国之间的一座桥梁,是欧洲科技人才的家园。欧洲科技圈通过特有的信息渠道集聚了最新最热的科技资讯、欧洲各国名校的本硕博留学信息以及博后和教职信息、国际公司的实习和职位信息、国内各地高层次人才计划以及高校和科研院所的招聘信息、国内创新创业各项扶持计划以及资助政策等对于欧洲华人息息相关的各类实用讯息。海内存知己,同样的经历让我们更了解你的需求。天涯若比邻,同样的梦想汇聚全欧华人英才。 作者:IlyaSuzdalnitski是加拿大阿尔伯塔省Calgary公司的资深全栈工程师。 本文剖析了为什么是时候远离OOP了。 OOP被许多人认为是计算机科学界皇冠上的明珠,是编程部门的终极解决方案,可以消除我们的所有问题,是编写程序的唯一实际方式,是上天赐予我们的编程法宝...... 实则不是,人们开始无法忍受抽象和随意共享的可变对象组成的复杂图形。宝贵的时间和脑力耗费在了思考“抽象”和“设计模式”上,而不是解决实际问题。 许多人批评面向对象编程(OOP),包括大名鼎鼎的软件工程师。就连OOP的发明者本人也猛烈炮轰现代OOP! 每个软件开发人员的终极目标应该是编写可靠的代码。如果代码有缺陷且不可靠,其他一切都不重要。编写可靠代码的最佳方法又是什么?简单性。简单性是复杂性的对立面。因此,我们软件开发人员的首要职责应该是降低代码复杂性。 免责声明 老实说,我不是面向对象编程的狂热粉丝。当然,这篇文章会有偏见。然而我不喜欢OOP有充分的理由。 我也明白抨击OOP是个很敏感的话题,可能会冒犯许多读者。然而,我认为我做的没错。我无意冒犯诸位,而是希望诸位对OOP带来的问题有更深刻的认识。 我无意批评AlanKay发明的OOP,他是个天才。我希望OOP以他设计的方式来实施。我批评的是现代Java/C#对OOP采用的做法。 我认为,OOP被许多人(包括技术高管)视为编程部门事实上的标准是不对的。许多主流语言除了OOP外不为编程部门提供其他任何替代方案也是不可接受的。 老实说,我以前在开发OOP项目时痛苦不堪,也搞不清楚为什么这么痛苦。可能是我不够优秀?我那时想必须学习更多的设计模式!最终,我彻底被搞得筋疲力尽。 此文总结了我从面向对象编程向函数式编程转型的长达十年的历程。遗憾的是,无论我怎么努力,再也无法OOP的使用场景。我个人发现OOP项目很失败,因为它们变得太复杂而无法维护。 长话短说 面向对象程序是作为正确程序的替代方案而提供的......——计算机科学界的先驱EdsgerW.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,甚至可能等于。Calculator对象的依赖项可能以微妙但深刻的方式改变计算的结果。真糟糕!需要一种弹性框架 优秀的程序员编写优秀的代码,糟糕的程序员编写错误的代码,无论是什么编程范式。然而,编程范式应限制糟糕的程序员造成太大的破坏。当然,你不在其中,因为你已经在阅读本文,并努力学习。糟糕的程序员从来没有时间学习,他们只是在键盘上疯狂地按下随机按键。无论你喜不喜欢,都会与糟糕的程序员共事,其中一些人糟透了。而且,遗憾的是,OOP没有足够的约束机制来防止糟糕的程序员造成太大的破坏。真糟糕!我不认为自己是糟糕的程序员,但如果没有一种强大的框架,我也编写不出优秀的代码。是的,一些框架与一些非常特殊的问题有关(比如Angular或ASP.Net)。我不是要谈论软件框架。我谈论的是框架更为抽象的字典定义:“一种必不可少的支持结构”——与更抽象的东西(比如代码组织和处理代码复杂性)有关的框架。尽管面向对象编程和函数式编程都是编程范式,但它们也都是很高级的框架。限制我们的选择C++是一种糟糕的[面向对象]语言.....将你的项目限制于C意味着人们不会因任何愚蠢的“对象模型”把事情搞砸。——Linux的开发者LinusTorvaldsLinusTorvalds因公开炮轰C++和OOP而广为人知。他完全正确的一点是,限制了程序员能做出的选择。实际上,程序员的选择越少,代码的弹性就越大。在上面那句引语中,LinusTorvalds强烈建议要有一种优秀的框架来构建我们的代码。许多人不喜欢道路上的限速牌,但它们对于帮助防止人被撞车至关重要。同样,一种优秀的编程框架应提供阻止我们做蠢事的机制。一种优秀的编程框架可以帮助我们编写可靠的代码。首先,它应通过提供以下几方面来帮助降低复杂性:模块性和可重用性 适当的状态隔离 高信噪比 遗憾的是,OOP为开发人员提供了太多的工具和选择,又没有予以适当类别的限制。尽管OOP承诺可支持模块性并提高可重用性,但未能兑现承诺(稍后将详细介绍)。OOP代码鼓励使用共享的可变状态,这已再三被证明不安全。OOP通常需要大量的样板代码(低信噪比)。函数式编程到底什么是函数式编程?一些人认为它是一种高度复杂的编程范式,仅适用于学术界,不适合“现实世界”。事实并非如此!是的,函数式编程有强大的数学基础,扎根于lambda演算。然而,它的大多数想法都是为应对更主流的编程语言中的弱点应运而生的。函数是函数式编程的核心抽象。如果使用得当,函数提供了一定程度的代码模块性和可重用性,这是OOP所从未见过的。它甚至还有解决为空性(nullability)问题的设计模式,提供了一种出色的错误处理方式。函数式编程做得很好的一方面是,它帮助我们编写可靠的软件。几乎完全不需要调试器。是的,无需单步调试代码并观察变量。我本人已很久没碰调试器了。最棒的方面是?如果你已经知道如何使用函数,那么早已是函数式程序员。你只要学习如何充分利用那些函数!我不是在宣传函数式编程,也不关心你使用什么编程范式来编写代码。我只是想表明函数式编程提供的机制可以解决OOP/命令式编程所固有的问题。我们都搞错了OOP 抱歉,我很久以前为这个话题首创了“对象”一词,因为它让许多人专注于这个次要的概念。重要的概念是消息传递。——OOP的发明者AlanKayErlang通常不被认为是面向对象的语言,但Erlang可能是唯一主流的面向对象语言。是的,Smalltalk当然是一种正宗的OOP语言——然而,它没有广泛使用。Smalltalk和Erlang都以发明者AlanKay最初设想的方式使用OOP。消息传递AlanKay在20世纪60年代首创了“面向对象编程”这个术语。他以生物学出身,因此试图使计算机程序如同活细胞那样进行联系。AlanKay的主要想法是通过向对方发送消息,从而让独立程序(细胞)进行联系。独立程序的状态永远不会与外界共享(封装)。就是这样。OOP从来没打算要有继承、多态性、“新”关键字和无数设计模式之类的东西。最纯粹的OOPErlang是最纯粹的OOP。与更主流的语言不同,它专注于OOP的核心思想:消息传递。在Erlang中,对象通过对象之间传递不可变消息进行联系。有没有证据表明与方法调用相比,不可变消息是一种出色的方法?当然有!Erlang可能是世界上最可靠的语言。它用于世界上大多数的电信以及互联网基础设施。用Erlang编写的一些系统的可靠性高达99.%,没错九个9!代码复杂性 使用OOP衍生而来的编程语言,计算机软件变得更冗长、可读性更低、描述性更差,更难修改和维护。——RichardMansfield软件开发最重要的方面是降低代码复杂性。就是这样。如果代码库变得无法维护,任何花哨的功能都不重要。如果代码库变得太复杂、不可维护,即使%的测试覆盖率也毫无价值。什么让代码库变得复杂?有很多因素要考虑,但在我看来,主要因素是:共享可变状态、错误的抽象以及低信噪比(常常由样板代码引起)。所有这些在OOP中司空见惯。状态的问题什么是状态?简而言之,状态是存储在内存中的任何临时数据。想想OOP中的变量或字段/属性。命令式编程(包括OOP)从程序状态方面来描述计算和对该状态的更改。声明性(函数式)编程改而描述所需的结果,不明确指定对状态的更改。可变状态——搞脑子的行为我认为,你在构建可变对象组成的庞大对象图形时,大型面向对象程序面临越来越复杂的难题。你知道,要设法理解并记住你在调用方法时会发生什么、会有什么副作用。——Clojure的发明者RichHickey状态本身无害。然而,可变状态是一大祸害,如果共享更是如此。可变状态究竟是什么?指可能会变的任何状态。想想OOP中的变量或字段。请给出实际例子!你有一张白纸,你在上面写下注释,最后得到不同状态(文字)的同一张纸。实际上,你已改变了那张纸的状态。这在现实世界中完全没问题,因为可能没有其他人关心那张纸,除非这张纸是《蒙娜丽莎》原画。人脑的局限性为什么可变状态是个大问题?人脑是已知宇宙中功能最强大的机器。然而,人脑在处理状态时很糟糕,因为我们在工作记忆中每次只能容纳5样东西。如果你只考虑代码的用途,而不是它在代码库方面改变了什么变量,就一段代码作推论要容易得多。用可变状态编程是一种搞脑子的行为。你我不知道,反正我只会同时耍两个球。要是给我三个或更多球,球肯定都会掉下来。那么为什么我们每天在工作中都要做这种搞脑子的事呢?遗憾的是,可变状态所需的搞脑子正是OOP的核心。对象方法存在的唯一目的是改变这同一个对象。零散的状态OOP将状态分散在整个程序中,因此使代码组织更为糟糕。然后在各个对象之间随意共享零散的状态。请给出实际例子!暂且忘了我们都是成年人,假装我们在尝试拼装一辆很酷的乐高积极卡车。然而有个问题——所有卡车零件与来自你其他乐高玩具的零件随机混合在一起。它们被再次随机地放入到50个不同的盒子中。还不允许你将卡车零件组合在一起——你得记住各个卡车零件的位置,还只能逐个取出零件。是的,你最终会拼装好那辆卡车,但要花多长时间?这与编程有什么关系?在函数式编程中,状态通常是孤立的。你始终知道某个状态来自何处。状态永远不会分散在你的不同函数中。在OOP中,每个对象都有自己的状态;构建一个程序时,你得记住目前处理的所有对象的状态。为了简化我们的生活,最好只有一小部分代码库处理状态。让应用程序的核心部分无状态且纯粹。这实际上是前端Flux模式(又名Redux)大获成功的主要原因。随意共享的状态好像嫌我们的生活因零散的可变状态还不够糟糕,OOP更进一步!请给出实际例子!现实世界中的可变状态几乎从来不是问题,因为事物是保密的,永不共享。这实际上是“适当的封装”。想象一位画家在绘下一幅蒙娜丽莎画。他在独自绘画,作最后的润饰,然后高价出售杰作。现在,他对钱感到了厌倦,决定做有点不一样的事。他认为举办绘画派对是好主意。他请来朋友们:精灵、甘道夫、警察和僵尸来帮助自己。这是团队合作!他们都同时在同一块画布上开始绘画。当然,这画不出什么佳作——这幅画是彻头彻尾的灾难!共享的可变状态在现实世界中毫无意义。然而,这正是出现在OOP程序中的一幕——状态在各个对象之间随意共享,而且以它们觉得合适的方式改变状态。反过来,随着代码库越来越大,这使得就程序作推论越来越难。并发问题OOP代码中随意共享可变状态使得这种代码的并行化几乎不可能。已经发明了复杂的机制来解决这个问题。已发明了线程锁定、互斥锁和其他许多机制。当然,这种复杂的方法有其自身的缺点:死锁、缺乏可组合性、调试多线程代码很困难很费时。我甚至没有谈到使用这种并发机制导致复杂性增加。并非所有状态都是坏的所有状态都是坏的?不,AlanKay设想的状态可能不是坏的!如果是真正孤立的(不是“OOP那样”的孤立),状态变异可能是好的。拥有不可变的数据传输对象也完全没问题。这里的关键是“不可变”。这种对象随后被用来在函数之间传递数据。然而,这种对象也会使OOP方法和属性完全冗余。如果对象无法变异,拥有对象的方法和属性又有什么用?可变性是OOP固有的一些人可能认为,可变状态是OOP中的一种设计选择,而非义务。这种说法有个问题。它不是一种设计选择,几乎是唯一的选择。是的,你在Java/C#中可以将不可变对象传递给方法,但由于大多数开发人员默认数据突变,因此很少这么做。即使开发人员试图在OOP程序中使用不可变性,语言也没有为不可变性提供内置机制,也没有为有效处理不可变数据提供机制(即持久数据结构)。是的,我们可以确保对象只通过传递不可变消息进行联系,永远不会传递任何引用(很少这么做)。这类程序会比主流OOP更可靠。然而一旦收到消息,对象仍然不得不改变自己的状态。消息是副作用,其唯一目的是引起更改。如果消息不能改变其他对象的状态,也就毫无用处。不可能在不引起状态变异的情况下使用OOP。封装的特洛伊木马 我们被告知封装是OOP的最大好处之一。封装理应保护对象的内部状态免受外部访问。但是这有一个小问题。它不管用。封装好比是OOP的特洛伊木马。它通过使其看起来安全来推销共享可变状态这个想法。封装允许、甚至鼓励不安全的代码潜入代码库,从而使代码库从里面腐烂。全局状态问题我们被告知全局状态是万恶之源。应不惜一切代价避免全局状态。我们从未被告知的是,封装实际上是美化的全局状态。为了使代码更高效,对象不是由其值传递的,而是由其引用传递的。“依赖项注入”(dependencyinjection)出问题就出在这里。让我解释一下。每当我们用OOP创建一个对象,会将对其依赖项的引用传递给构造函数。那些依赖项也有自己的内部状态。刚创建的对象将那些依赖项的引用愉快地存储在内部状态中,然后愉快地以它高兴的任何方式修改。它还将那些引用一路传递给它可能最终使用的任何其他内容。这会创建由随意共享的对象组成的复杂图形,这些对象最终改变彼此的状态。反过来,这会导致巨大的问题,因为几乎不可能看到什么导致程序状态改变。试图调试这种状态更改可能浪费好几天。如果你不必处理并发性,算你走运(稍后会详细介绍)。方法/属性提供对特定字段访问的方法或属性与直接更改字段的值一样差劲。是否通过使用花哨的属性或方法来改变对象的状态并不重要——结果都一样:变异状态。现实世界建模存在的问题 有人说OOP试图为现实世界建模。事实根本不是这样——OOP在现实世界中与任何事物没有关系。试图将程序建模成对象可能是OOP的最大错误之一。现实世界不是层次结构的OOP尝试将一切建模成对象的层次结构。遗憾的是,现实世界中的事物不是这样子。现实世界中的对象使用消息彼此联系,但它们大多彼此独立。现实世界中的继承OOP继承并不是仿照现实世界。现实世界中的父对象无法在运行时改变子对象的行为。即使你从父母那里继承了DNA,他们也无法随心所欲地改变你的DNA。你不是从父母那里继承“行为”,你发展出自己的行为。你也无法“凌驾”你父母的行为。现实世界没有方法你写的那张纸有没有“写”方法?没有!你拿来一张空纸,拿起一支笔,然后写下一些文字。作为一个人,你也没有“写”方法——你根据外部事件或自身的内部想法来决定写一些文字。名词的王国 对象以不可分割的单位将函数和数据结构绑定在一起。我认为这是根本性的错误,因为函数和数据结构属于全然不同的世界。——Erlang的发明者JoeArmstrong对象(或名词)是OOP的核心。OOP的一个基本限制是,它将一切强行当成名词。而并非一切都应建模成名词。不应将操作(函数)建模成对象。既然我们只需要乘以两个数的函数,为什么我们被迫创建一个Multiplier类?只需一个Multiply函数,让数据是数据,让函数是函数!在非OOP语言中,执行将数据保存到文件之类的简易操作简单直观——酷似你用简单的英语来描述动作。请给出实际例子!当然,回到画家那个例子,画家拥有PaintingFactory。他请来了专门的BrushManager、ColorManager、CanvasManager和MonaLisaProvider。他的好朋友僵尸使用BrainConsumingStrategy。反过来,那些对象定义了下列方法:CreatePainting、FindBrush、PickColor、CallMonaLisa和ConsumeBrainz。当然,这很简单,在现实世界中永远不会发生。为绘画这个简单的行为创造了多少不必要的复杂性?如果允许函数与对象独立存在,不需要发明奇怪的概念来保存函数。单元测试 自动化测试是开发过程的重要组成部分,可极大地帮助防止回归(即错误被引入到现有代码中)。单元测试在自动化测试过程中发挥了巨大的作用。一些人可能不同意,但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 试图理解开发人员自己构建的塔状抽象浪费了宝贵的脑力。在许多情况下,没有结构胜过拥有糟糕的结构(如果你问我的话)。图片来源:北京白癜风医院哪家最专业北京白癜风医院哪家最专业转载请注明原文网址:http://www.gzdatangtv.com/bcyyfz/12596.html |