还在面向对象编程?是时候说再见了!

21ic电子网 2020-05-20 00:00

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

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


几十年来我都在用面向对象的语言编程。我用过的第一个面向对象的语言是 C++,后来是 Smalltalk,最后是 .NET 和 Java。
我曾经对使用继承、封装和多态充满热情。它们是范式的三大支柱。
我渴望实现重用之美,并在这个令人兴奋的新天地中享受前辈们积累的智慧。
想到将现实世界的一切映射到类中,使得整个世界都可以得到整齐的规划,我无法抑制自己的兴奋。
然而我大错特错了。
 01 
继承,倒塌的第一根支柱
乍一看,继承似乎是面向对象范式的最大优势。所有新手教程讲解继承时都会拿出最简单的继承的例子,而这个例子似乎很符合逻辑。
然后就是满篇的重用了。甚至以后的一切都是重用了。
我囫囵吞下这一切,然后带着新发现兴冲冲地奔向世界了。
香蕉猴子丛林问题
带着满腔的信仰和解决问题的热情,我开始构建类的层次结构然后写代码。似乎一切皆在掌控中。
我永远不会忘记我准备从已有的类继承并实现重用的那一天。那是我期待已久的时刻。
后来有了新的项目,我想起了另一个项目里我很喜欢的那个类。
没问题,重用拯救一切。我只需要把那个类拿过来用就好了。
嗯……其实……不仅是那一个类。还得把父类也拿过来。但……应该就可以了吧。
额……不对,似乎还需要父类的父类……还有……嗯,我们需要所有的祖先类。好吧好吧……搞定了。没问题。
不错。但编译不过,怎么回事?哦我知道了……这个对象还需要另一个对象。所以那个也得拿过来。没问题……
等等……我不仅需要那个对象,还需要那个对象的父类,和父类的父类,和……包含的所有对象的所有祖先……
唉……
Erlang 的创建者 JoeArmstrong 有句名言:
面向对象语言的问题在于,它们依赖于特定的环境。你想要个香蕉,但拿到的却是拿着香蕉的猩猩,乃至最后你拥有了整片丛林。
香蕉猴子丛林的解决方法
这个问题的解决方法是,不要把类层次建得那么深。但如果继承是重用的关键,那么给继承机制添加的任何限制都会限制重用。对吧?
没错。
那我们可怜的面向对象程序员该怎么办?指望一杯三聚氰胺奶维系我们的健康吗?
答案就是:包含和委托(Contain and Delegate)。一会儿会详细解释。
菱形继承问题
早晚你会遇到下面这种恶心的问题,有些语言甚至根本解决不了。
大多数面向对象语言都不支持这种情况,尽管看上去似乎很符合逻辑。为什么面向对象语言支持这种情况如此困难?
来看看下面的伪代码:
    
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier inherits from ScannerPrinter {
}
注意 Scanner 和 Printer 类都实现了名为 start 方法。
那么问题来了,Copier继承哪个start?是Scanner的还是Printer的?肯定不可能同时继承啊。
菱形继承的解决
解决方案很简单:不要这样做。
没错。大多数面向对象都不让你这么干。
但是,但是……要是必须这样建模该怎么办?我需要重用!
那就必须使用包含和委托
    
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier {
  Scanner scanner
  Printer printer
  function start() {
    printer.start()
  }
}
注意现在 Copier 类包含一个 Printer 实例和一个 Scanner 实例。然后将 start 函数委托给 Printer 类的实现。要委托给 Scanner 也很简单。
这个问题是继承这根支柱上的另一条裂缝。
脆弱的基类问题
好吧,那我尽量使用较浅的类层次结构,并保证里面没有环,这样就不会出现菱形继承了。
似乎一切都解决了。直到我们发现……
我前一天工作得好好的代码今天出错了!关键是,我没有改任何代码!
嗯也许是个 bug……但等等……的确有些改动……
但改动的不是我的代码。似乎改动来自我继承的那个类。
为什么基类的改动会破坏我的代码?
原来是这样……
看看下面这个基类(用Java写的,但就算你不懂Java,应该也很容易看懂):
    
import java.util.ArrayList;

public class Array
{
  private ArrayList<Object> a = new ArrayList<Object>();

  public void add(Object element)
  
{
    a.add(element);
  }

  public void addAll(Object elements[])
  
{
    for (int i = 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  
那该怎么办?
去拥抱函数式编程吧。过去几年我用得非常舒服。
但话说在先,我并没有给你做出任何承诺。眼见为实。
一朝被蛇咬十年怕井绳。
你懂的!

-END-

来源:CSDN



推荐阅读

【1】雷军喜提第4家上市公司,又送出一公斤金砖

【2】温故知新!六款简单的开关电源电路设计

【3】终于整理齐了,电子工程师“设计锦囊”,你值得拥有!

【4】半导体行业的人都在关注这几个公众号


你和大牛工程师之间到底差了啥?
加入技术交流群,与高手面对面 
添加管理员微信

加入“中国电子网微信群”交流

具体加群详情请戳
“中国电子网技术交流群” 
21ic电子网 即时传播最新电子科技信息,汇聚业界精英精彩视点。
评论
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 86浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 51浏览
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 70浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 41浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 65浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 66浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 101浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 83浏览
  • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
    艾迈斯欧司朗 2024-11-29 16:55 168浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 105浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 38浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 73浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 106浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦