0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

你是使用函数式编程还是面向对象编程方式?

GReq_mcu168 来源:CSDN知识库 2020-06-05 15:15 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

作为程序员,你是使用函数式编程还是面向对象编程方式?

在本文中,拥有 10 多年软件开发经验的作者从面向对象编程的三大特性——继承、封装、多态三大角度提出了自己的疑问,并深刻表示是时候和面向对象编程说再见了。

几十年来我都在用面向对象的语言编程。我用过的第一个面向对象的语言是 C++,后来是 Smalltalk,最后是 .NET 和 Java。 我曾经对使用继承、封装和多态充满热情。它们是范式的三大支柱。 我渴望实现重用之美,并在这个令人兴奋的新天地中享受前辈们积累的智慧。 想到将现实世界的一切映射到类中,使得整个世界都可以得到整齐的规划,我无法抑制自己的兴奋。 然而我大错特错了。
01继承,倒塌的第一根支柱 乍一看,继承似乎是面向对象范式的最大优势。所有新手教程讲解继承时都会拿出最简单的继承的例子,而这个例子似乎很符合逻辑。

然后就是满篇的重用了。甚至以后的一切都是重用了。 我囫囵吞下这一切,然后带着新发现兴冲冲地奔向世界了。香蕉猴子丛林问题带着满腔的信仰和解决问题的热情,我开始构建类的层次结构然后写代码。似乎一切皆在掌控中。 我永远不会忘记我准备从已有的类继承并实现重用的那一天。那是我期待已久的时刻。 后来有了新的项目,我想起了另一个项目里我很喜欢的那个类。 没问题,重用拯救一切。我只需要把那个类拿过来用就好了。 嗯……其实……不仅是那一个类。还得把父类也拿过来。但……应该就可以了吧。 额……不对,似乎还需要父类的父类……还有……嗯,我们需要所有的祖先类。好吧好吧……搞定了。没问题。 不错。但编译不过,怎么回事?哦我知道了……这个对象还需要另一个对象。所以那个也得拿过来。没问题…… 等等……我不仅需要那个对象,还需要那个对象的父类,和父类的父类,和……包含的所有对象的所有祖先…… 唉…… Erlang 的创建者 JoeArmstrong 有句名言:

面向对象语言的问题在于,它们依赖于特定的环境。你想要个香蕉,但拿到的却是拿着香蕉的猩猩,乃至最后你拥有了整片丛林。

香蕉猴子丛林的解决方法这个问题的解决方法是,不要把类层次建得那么深。但如果继承是重用的关键,那么给继承机制添加的任何限制都会限制重用。对吧? 没错。 那我们可怜的面向对象程序员该怎么办?指望一杯三聚氰胺奶维系我们的健康吗? 答案就是:包含和委托(Contain and Delegate)。一会儿会详细解释。菱形继承问题早晚你会遇到下面这种恶心的问题,有些语言甚至根本解决不了。

大多数面向对象语言都不支持这种情况,尽管看上去似乎很符合逻辑。为什么面向对象语言支持这种情况如此困难? 来看看下面的伪代码:

ClassPoweredDevice{ } ClassScannerinheritsfromPoweredDevice{ functionstart(){ } } ClassPrinterinheritsfromPoweredDevice{ functionstart(){ } } ClassCopierinheritsfromScanner,Printer{ } 注意 Scanner 和 Printer 类都实现了名为 start 方法。 那么问题来了,Copier继承哪个start?是Scanner的还是Printer的?肯定不可能同时继承啊。菱形继承的解决解决方案很简单:不要这样做。 没错。大多数面向对象都不让你这么干。 但是,但是……要是必须这样建模该怎么办?我需要重用! 那就必须使用包含和委托。ClassPoweredDevice{ } ClassScannerinheritsfromPoweredDevice{ functionstart(){ } } ClassPrinterinheritsfromPoweredDevice{ functionstart(){ } } ClassCopier{ Scannerscanner Printerprinter functionstart(){ printer.start() } } 注意现在 Copier 类包含一个 Printer 实例和一个 Scanner 实例。然后将 start 函数委托给 Printer 类的实现。要委托给 Scanner 也很简单。 这个问题是继承这根支柱上的另一条裂缝。脆弱的基类问题好吧,那我尽量使用较浅的类层次结构,并保证里面没有环,这样就不会出现菱形继承了。 似乎一切都解决了。直到我们发现…… 我前一天工作得好好的代码今天出错了!关键是,我没有改任何代码! 嗯也许是个 bug……但等等……的确有些改动…… 但改动的不是我的代码。似乎改动来自我继承的那个类。 为什么基类的改动会破坏我的代码? 原来是这样…… 看看下面这个基类(用Java写的,但就算你不懂Java,应该也很容易看懂):importjava.util.ArrayList; publicclassArray { privateArrayLista=newArrayList(); publicvoidadd(Objectelement) { a.add(element); } publicvoidaddAll(Objectelements[]) { for(inti=0;i< elements.length; ++i)       a.add(elements[i]); // this line is going to be changed   } }重要提示:注意加了注释的那一行。稍后这行的改动将会导致别的东西出错。  这个类的接口上有两个函数:add() 和 addAll()。add() 函数负责添加一个元素,addAll() 函数会调用 add 函数添加多个元素。  下面是继承的类:public class ArrayCount extends Array {   private int count = 0;   @Override   public void add(Object element)   {     super.add(element);     ++count;   }   @Override   public void addAll(Object elements[])   {     super.addAll(elements);     count += elements.length;   } } ArrayCount类是通用的Array类的特化。两者行为上的唯一区别就是ArrayCount会维护一个count,记录元素的个数。 我们来仔细看看这两个类。 Array的add()给局部的ArrayList添加一个元素。 Array的addAll()针对每个元素调用局部的ArrayList的add方法。 ArrayCount的add()调用父类的add()然后增加count。 ArrayCount的addAll()调用父类的addAll()然后给count增加相当于元素个数的数。 一切都很正常。 现在是出问题的地方。基类中加注释的那行代码现在改成这样:public void addAll(Object elements[])   {     for (int i = 0; i < elements.length; ++i)       add(elements[i]); // this line was changed   } 从基类的作者的角度来看,这个类实现的功能完全没有变化。而且所有自动化测试也都通过来了。 但是基类的作者忘记了继承的类。而继承类的作者被错误吵醒了。 现在ArrayCount的addAll()调用父类的addAll(),后者在内部调用add(),而add()被继承类重载了。 因此,每次继承类的add()被调用时,count都会增加,然后在继承类的addAll()被调用时再次增加。 count被增加了两次。 既然会发生这种现象,那么继承类的作者必须清楚基类是怎样实现的。而且,基类的每个改动必须要通知所有继承类的作者,因为这些改动可能会以不可预知的方式破坏继承类。 唉!这个巨大的裂隙威胁到了整个继承支柱的稳定。脆弱的基类的解决方法这个问题还得要包含和委托来解决。 使用包含和委托,可以从白盒编程转到黑盒编程。白盒编程的意思是说,写继承类时必须要了解基类的实现。 而黑盒编程可以完全无视基类的实现,因为不可能通过重载函数的方式向基类注入代码。只需要关注接口即可。 这种趋势太讨厌了…… 继承本应带来最好用的重用。 在面向对象语言中实现包含和委托并不容易。它们是为了继承方便而设计的。 如果你和我一样,你就会开始反思这个继承了。但更重要的是,这些问题应当引起你对于通过层次结构进行分类的反思。层次结构的问题每到一个新公司时,我都要为在哪儿保存公司文档(即员工手册)而纠结。 是应该建一个Documents文件夹,然后在里面建个Company呢? 还是应该建个Company文件夹,然后在里面建个Documents呢? 两者都可以。但哪个是正确的?哪个更好? 层次分类的思想是因为基类(父类)更通用,继承类(子类)更专用。沿着继承链越往下走,概念就越专用(见上面的形状层次)。 但如果父节点和子节点能随意交换位置,那么显然这种模型是有问题的。层次结构的解决真正的问题出在……层次分类是错误的。那层次分类应该用在哪里?包含关系。真实世界里有很多包含关系(或者叫做独占关系)的层次结构。 但你找不到层次分类。仔细想一下。面向对象范式是根据充满了各种对象的真实世界建立的。但它用错了模型——层次分类在真实世界中没有类比。 但真实世界里到处都是层次包含关系。层次包含关系的一个非常好的例子就是你的袜子。袜子放在装袜子的抽屉里,然后抽屉包含在衣柜里,衣柜包含在卧室里,卧室包含在房子里,等等。  硬盘上的目录也是层次包含关系的另一个例子——它们包含文件。 那我们该怎样分类呢? 仔细想一下公司文档,就会发现其实放在哪儿都无所谓。我可以放在Documents目录下或者放在Stuff目录下也可以。 我选择的分类法是标签。我给它加上不同的标签。Document Company Handbook 标签是没有顺序或层次的(这同时解决了菱形继承问题)。 标签可以类比为接口,因为同一份文档可以有多种类型。 但既然有了这么多裂缝,估计继承的支柱已经倒塌了。  再见,继承。   02  封装,倒塌的第二根支柱 乍一看,封装似乎是面向对象编程的第二大好处。 对象状态变量被保护起来防止外部访问,即它们被封装在对象内部。 我们不需要再操心那些可能被不知道谁访问的全局变量。 封装是变量的保险柜。 封装太伟大了! 封装万岁……  直到你遇到了这个问题……引用问题为了提高效率,对象传递给函数时传递的是引用,而不是值。 也就是说,函数不会传递对象本身,而是传递指向对象的一个引用或指针。 如果一个对象的引用被传递给另一个对象的构造函数,构造函数就能将这个对象引用放到私有变量中,用封装保护起来。 但这个传递的对象不是安全的! 为什么不是?因为其他代码也可能拥有指向该对象的指针,比如调用构造函数的那段代码。它必须有指向对象的引用,否则没办法传递给构造函数。引用的解决构造函数必须要复制传递过来的对象。而且不能是浅复制,必须是深复制,即传入的对象内包含的所有对象和所有对象中包含的所有对象……都必须要复制。 完全没有效率。 而且更糟糕的是,并非所有对象都能复制的。一些拥有操作系统资源的对象,最好的情况是复制无效,最糟糕的情况是根本不可能复制。 所有主流面向对象语言都有这个问题。  再见,封装。   03  多态,倒塌的第三根支柱  多态是面向对象的三位一体中永远被人抛弃的那一位。 就像是三人组中的Larry Fine。 不管他们去哪儿都会带着他,但他永远是配角。 并不是因为多态不好,而是因为实现多态并不需要面向对象语言。 接口也能实现多态,而且不需要面向对象的负担。 而且,接口也不会限制你能混入的不同行为的数目。  所以,无需多言,我们可以告别面向对象的多态,去迎接基于接口的多态吧。   04  破碎的承诺  当然,面向对象在早期承诺了许多。而直到今天,这些承诺依然在教室里、博客上和网上资源中传授给青涩的程序员们。 我花了多年才意识到面向对象的谎言。以前我也曾经青涩,曾经轻信。 然后我发现被骗了。 再见,面向对象编程。   05  那该怎么办?  去拥抱函数式编程吧。过去几年我用得非常舒服。 但话说在先,我并没有给你做出任何承诺。眼见为实。 一朝被蛇咬十年怕井绳。 你懂的!

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 封装
    +关注

    关注

    128

    文章

    9329

    浏览量

    149039
  • 编程
    +关注

    关注

    90

    文章

    3723

    浏览量

    97434
  • C++
    C++
    +关注

    关注

    22

    文章

    2129

    浏览量

    77363

原文标题:面向对象编程,再见!

文章出处:【微信号:mcu168,微信公众号:硬件攻城狮】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    线性化编程与结构化编程的不同点

    线性化编程是将整个用户程序连续放置在一个循环程序块(OB1)中,按顺序执行的编程范式。这种结构与PLC所代替的硬接线继电器控制类似,CPU逐条地处理指令,体现了早期PLC编程的简单性和直观性。说白了就是一条路走到黑,所有功能从上
    的头像 发表于 03-16 16:58 504次阅读
    线性化<b class='flag-5'>编程</b>与结构化<b class='flag-5'>编程</b>的不同点

    嵌入开发常用函数速查表

    在嵌入开发中,掌握常用函数的用法可以大大提高开发效率。无论是单片机初学者还是有一定经验的工程师,熟悉函数库和调用方式都是必备技能。今天,我
    的头像 发表于 01-19 09:06 464次阅读
    嵌入<b class='flag-5'>式</b>开发常用<b class='flag-5'>函数</b>速查表

    C语言嵌入系统编程注意事项-内存操作

    启动后第一条要执行的指令的位置。 记住:函数无它,唯指令集合耳;可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令! 数组vs动态申请 在嵌入
    发表于 01-04 07:31

    C语言与C++的区别及联系

    class等面向对象的特性和机制。但是,后来经过一步步修订和很多次演变,最终才形成了现如今这个支持一系列重大特性的庞大编程语言。 一、C语言是面向过程语言,而C++是
    发表于 12-24 07:23

    C语言和C++之间的区别是什么

    区别 1、面向对象编程 (OOP): C语言是一种面向过程的语言,它强调的是通过函数将任务分解为一系列步骤进行执行。 C++在C语言的基础
    发表于 12-11 06:23

    单片机C语言编程的心得

    写这个8*8按键程序的过程中,不管是在自己写还是参考别人程序的过程中,发现自己对C语言有些基本知识点和编程规范有很多不懂的地方,有些是自己以前的编程习惯不好,有些就是基础知识不扎实的表现,所以总结
    发表于 12-08 07:44

    一个面向单片机、事件驱动的嵌入开发平台介绍

    EventOS,是一个面向单片机、事件驱动的嵌入开发平台。它主要有两大技术特色:一是事件驱动,二是超轻量。EventOS以及其母项目EventOS,目标是开发一个企业级的嵌入开发平台,以事件总线
    发表于 12-05 06:26

    C语言的编程技巧

    、_Alignas关键字‌:C11标准引入了_Alignas关键字,用于显指定类型的对齐方式,优化内存访问效率。 ‌5、_Generic关键字‌:C11引入的_Generic关键字用于条件编译时的类型检查,提高
    发表于 11-27 06:46

    使用J-Flash来编程CW32 MCU

    。 确保连接正确,并且MCU处于可编程状态(例如,处于复位状态)。 3.启动J-Flash: 打开J-Flash应用程序。 在J-Flash中,选择正确的目标设备(即的CW32 MCU型号)。这通常
    发表于 11-25 07:00

    2025年最佳的嵌入编程语言有哪些呢?

    嵌入系统是现代科技不可或缺的一部分。它们存在于家用电器、汽车、住宅、医院、商店等各个领域。它们与我们的社会紧密相连。选择合适的语言来构建嵌入系统对于成功至关重要。那么,2025年最佳的嵌入
    的头像 发表于 11-14 10:27 1761次阅读
    2025年最佳的嵌入<b class='flag-5'>式</b><b class='flag-5'>编程</b>语言有哪些呢?

    信捷XS STUDIO编程软件V2.3.2版本的全新功能

    XS Studio(V2.3.2)编程软件,是面向XS系列的编程组态软件,集成了PLC编程、可视化HMI、安全PLC、控制器实时核、现场总线及运动控制功能,提供了一套完整的包括配置、
    的头像 发表于 09-20 14:19 2458次阅读
    信捷XS STUDIO<b class='flag-5'>编程</b>软件V2.3.2版本的全新功能

    C语言中的内联函数与宏

    在C编程中,内联函数和宏都用于避免函数调用的开销并编写可复用的逻辑部分,但它们在工作方式和安全性方面存在显著差异。
    的头像 发表于 07-25 15:10 2065次阅读
    C语言中的内联<b class='flag-5'>函数</b>与宏

    【HZ-T536开发板免费体验】2 - 交叉编译仓颉编程语言程序到开发板运行

    。以下是对它的详细介绍: 特点: 高效编程 :支持函数、命令面向对象等多种
    发表于 07-16 21:27

    深入理解C语言:函数编程中的“积木块”艺术

    编程的世界里,函数就像建筑中的“积木块”——它们是构建复杂程序的基石。通过灵活组合这些模块,开发者能打造出功能强大且结构清晰的代码。函数之所以成为C语言的核心,正是因为它解决了编程
    的头像 发表于 06-30 17:26 2032次阅读
    深入理解C语言:<b class='flag-5'>函数</b>—<b class='flag-5'>编程</b>中的“积木块”艺术

    求助,关于以编程方式配置DiplayPort MODES UFP_D引脚配置响应的疑问求解

    我想这个问题以前可能有人问过,但现在还是要问: 在 Host SDK 3.5(或更高版本)中,有什么最佳方法可以以编程方式覆盖 DP SINK / UFP 底座的 DisplayPort MODES
    发表于 05-21 07:28