面向对象编程,再见!

继承(六)—— 菱形继承的问题以及解决方案(初识虚基表) 了解菱形继承的问题以及解决方案 阅读详情


作者:Python大本营
来源:Python大本营

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

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

几十年来我都在用面向对象的语言编程。我用过的第一个面向对象的语言是 C++,后来是 Smalltalk,最后是 .NET 和 Java。

我曾经对使用继承、封装和多态充满热情。它们是范式的三大支柱。

我渴望实现重用之美,并在这个令人兴奋的新天地中享受前辈们积累的智慧。

想到将现实世界的一切映射到类中,使得整个世界都可以得到整齐的规划,我无法抑制自己的兴奋。

然而我大错特错了。

111

1

继承,倒塌的第一根支柱

乍一看,继承似乎是面向对象范式的最大优势。所有新手教程讲解继承时都会拿出最简单的继承的例子,而这个例子似乎很符合逻辑。

然后就是满篇的重用了。甚至以后的一切都是重用了。

我囫囵吞下这一切,然后带着新发现兴冲冲地奔向世界了。

香蕉猴子丛林问题

带着满腔的信仰和解决问题的热情,我开始构建类的层次结构然后写代码。似乎一切皆在掌控中。

我永远不会忘记我准备从已有的类继承并实现重用的那一天。那是我期待已久的时刻。

后来有了新的项目,我想起了另一个项目里我很喜欢的那个类。

没问题,重用拯救一切。我只需要把那个类拿过来用就好了。

嗯……其实……不仅是那一个类。还得把父类也拿过来。但……应该就可以了吧。

额……不对,似乎还需要父类的父类……还有……嗯,我们需要所有的祖先类。好吧好吧……搞定了。没问题。

不错。但编译不过,怎么回事?哦我知道了……这个对象还需要另一个对象。所以那个也得拿过来。没问题……

等等……我不仅需要那个对象,还需要那个对象的父类,和父类的父类,和……包含的所有对象的所有祖先……

唉……

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 Scanner, Printer {
}

注意 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

标签是没有顺序或层次的(这同时解决了菱形继承问题)。

标签可以类比为接口,因为同一份文档可以有多种类型。

但既然有了这么多裂缝,估计继承的支柱已经倒塌了。 

再见,继承。

2

封装,倒塌的第二根支柱 

乍一看,封装似乎是面向对象编程的第二大好处。

对象状态变量被保护起来防止外部访问,即它们被封装在对象内部。

我们不需要再操心那些可能被不知道谁访问的全局变量。

封装是变量的保险柜。

封装太伟大了!

封装万岁…… 

直到你遇到了这个问题……

引用问题

为了提高效率,对象传递给函数时传递的是引用,而不是值。

也就是说,函数不会传递对象本身,而是传递指向对象的一个引用或指针。

如果一个对象的引用被传递给另一个对象的构造函数,构造函数就能将这个对象引用放到私有变量中,用封装保护起来。

但这个传递的对象不是安全的!

为什么不是?因为其他代码也可能拥有指向该对象的指针,比如调用构造函数的那段代码。它必须有指向对象的引用,否则没办法传递给构造函数。

引用的解决

构造函数必须要复制传递过来的对象。而且不能是浅复制,必须是深复制,即传入的对象内包含的所有对象和所有对象中包含的所有对象……都必须要复制。

完全没有效率。

而且更糟糕的是,并非所有对象都能复制的。一些拥有操作系统资源的对象,最好的情况是复制无效,最糟糕的情况是根本不可能复制。

所有主流面向对象语言都有这个问题。 

再见,封装。

3

多态,倒塌的第三根支柱

多态是面向对象的三位一体中永远被人抛弃的那一位。

就像是三人组中的Larry Fine。

不管他们去哪儿都会带着他,但他永远是配角。

并不是因为多态不好,而是因为实现多态并不需要面向对象语言。

接口也能实现多态,而且不需要面向对象的负担。

而且,接口也不会限制你能混入的不同行为的数目。 

所以,无需多言,我们可以告别面向对象的多态,去迎接基于接口的多态吧。

4

破碎的承诺

当然,面向对象在早期承诺了许多。而直到今天,这些承诺依然在教室里、博客上和网上资源中传授给青涩的程序员们。

我花了多年才意识到面向对象的谎言。以前我也曾经青涩,曾经轻信。

然后我发现被骗了。

再见,面向对象编程。

5

那该怎么办?

去拥抱函数式编程吧。过去几年我用得非常舒服。

但话说在先,我并没有给你做出任何承诺。眼见为实。

一朝被蛇咬十年怕井绳。

你懂的!

(完)

别再为OpenBCI_GUI安装发愁了!保姆级教程带你从Processing配置到成功运行(附常见错误解决) 本文提供了一份详细的OpenBCI_GUI安装教程,涵盖从Processing配置到成功运行的全过程,特别针对常见错误提供了解决方案。通过环境搭建、软件依赖安装、源码处理及首次运行实战,帮助用户高效完成数据可视化工具的部署与使用。 阅读详情

相关推荐

【ISP图像处理】Demosaic去马赛克技术演进与边缘优化策略

本文深入解析了ISP图像处理中的Demosaic(去马赛克)技术。从Bayer阵列原理出发,梳理了从双线性插值、Hamilton-Adams边缘导向算法到利用通道相关性与自相似性的技术演进历程,并重点探讨了针对边缘与纹理伪影的优化策略,为工程实践中的算法选型与硬件协同设计提供了专业指导。

weixin_29091445的博客 388

菱形继承--菱形虚拟继承

菱形继承--菱形虚拟继承 菱形继承 菱形继承,其继承模型如下: 菱形继承存在问题,Teacher类和Student类都继承了Person类,所以Teacher类和Student类除了拥有自身的成员变量都还拥有继承于Person的成员变量。 而Assistant类继承了Teacher类和Student类,所以,Assistant类除了拥有自身的数据,还拥有Te

Zhang_1218的博客 539

期权专题9:雪球期权(一)(BSM+蒙特卡罗定价)

python;雪球定价;蒙特卡罗

mhmh123789的博客 8104

面向对象编程(全)

1,对象概述。 2,三大基本特性。3,面向对象高级特性 4,接口。 5,枚举。 6,内部类。

m0_70444867的博客 8698

在 NVIDIA Orin (JetPack 6.0) 上配置 PyTorch 2.4 + Torchvision 0.19 的完整避坑指南

本文提供在NVIDIA Jetson Orin设备上,基于JetPack 6.0系统配置PyTorch 2.4与Torchvision 0.19环境的完整指南。通过使用Miniconda环境隔离、安装特定版本PyTorch夜间构建版、编译匹配的Torchvision源码等步骤,解决版本不匹配、CUDA支持等常见问题,确保AI开发环境稳定可用。

weixin_29179311的博客 364

解锁RK3588视觉潜能:RGA硬件加速库实战指南

本文深入探讨了如何利用RK3588芯片内置的RGA硬件加速库,解决视觉应用中图像预处理的性能瓶颈。通过实战指南,详细介绍了RGA库在YUV转RGB、图像缩放等核心操作上的应用,相比CPU软处理可大幅降低延迟与CPU占用,从而充分释放RK3588的NPU算力,构建高效的AI视觉预处理流水线。

zeta9的博客 669

面向对象编程

JavaSE面向对象基础 重点回顾 数组创建 动态创建:数据类型[ ] 数组变量名=new 数据类型 [元素个数] 静态创建:数据类型[ ] 数组变量名= [元素1,元素2…] 动态创建并赋初始值:数据类型[ ] 数组变量名=new 数据类型 [ ] {元素1,元素2…} 数组特点: 只能存储相同类型的数据 数组中的值可以重复 数组的长度是固定的 数组是连续的内存空间 系统预定义类型引入 基本类型及包装类 字符串类型 日期类型基础 随机数 Math.random() Random r

clear_0217的博客 3514

Apache-Plc4x-Modbus-Tcp学习记录(一)

Apache-Plc4x-Modbus-Tcp学习记录(一)

weixin_45325423的博客 6988

对象必须实现 iconvertible_面向对象编程,再见!再见!再见!

作为程序员,你是使用函数式编程还是面向对象编程方式?在本文中,拥有 10 多年软件开发经验的作者从面向对象编程的三大特性——继承、封装、多态三大角度提出了自己的疑问,并深刻表示是时候和面向对象编程说再见了。几十年来我都在用面向对象的语言编程。我用过的第一个面向对象的语言是 C++,后来是 Smalltalk,最后是 .NET 和 Java。我曾经对使用继承、封装和多态充满热情。它们是范式的三大支柱...

weixin_39803552的博客 364

二十一、CANdelaStudio深入-SPRMIB的配置

本专栏将由浅入深的展开诊断实际开发与测试的数据库编辑,包含大量实际开发过程中的步骤、使用技巧与少量对Autosar标准的解读。希望能对大家有所帮助,与大家共同成长,早日成为一名车载诊断、通信全栈工程师。 本文介绍CANdelaStudio的肯定响应抑制位配置,欢迎各位朋友订阅、评论,可以提出宝贵意见,以持续输出更好的作品,同时会根据评论、私信不定期更新文章内容,以及专栏目录。订阅该专栏的朋友可私信小编免费答疑解惑(^U^)ノ~YO CANdelaStudio-从入门到深入目录 文章目录CANdelaStu

诊断协议那些事儿's blog 1870

配置 SAP Fiori 服务的 ICF 节点:从 SICF 激活到访问链路打通的完整实践

在SAP Fiori项目实施中,ICF节点配置是关键前置条件却常被忽视。ICF(Internet Communication Framework)作为HTTP请求入口,需要通过SICF事务码激活相关服务节点,否则即使应用开发完善,用户仍可能遇到页面无法访问等问题。典型问题表现为:磁贴可点击但页面空白、资源加载失败等。激活路径主要涉及三组节点:bsp分支下的管理入口、ui5_ui5分支下的应用资源,以及public/bc下的公共资源。项目实施中需特别注意同名服务在不同分支下的差异化作用,确保完整链路通畅。IC

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 264

[ZZ]面向对象编程,再见!

面向对象编程,再见!- 机器学习算法与自然语言处理 https://mp.weixin.qq.com/s/icXBlVOOYLvDnER7cEeCeg https://medium.com/@cscalfani/goodbye-object-oriented-programming-a59cda4c0e53 作为程序员,你是使用函数式编程还是面向对象编程方式?在本文中,拥有 10 多年软...

weixin_34397291的博客 150

【2025软考高级架构师】基础知识部分——6、软件架构设计

(1)架构是对系统的抽象(2)架构由多个结构组成(3)任何软件都存在架构(4)元素及其行为的集合构成架构的内容(5)架构具有“基础”性(6)架构隐含有“决策”

qq837993702的博客 1万+

再见,面向对象编程!(Goodbye, Object Oriented Programming)

左耳听风 第一周 Review.png 原文:Goodbye, Object Oriented Programming原作者:Charles Scalfani 原文介绍 作为使用面向对象编程已有十几年的作者,曾经也非常热衷于继承、封装、多态的优点。它们是范式的三大支柱。但是随着对面向对象编程的深入理解,结合实际遇到的问题,带着挑剔的眼光,作者看到了面向对象编程的一些问题。 *继承,堕落...

墨1024 348

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

再见,面向对象编程

文章来源于公众号 动画工程师,翻译自 Charles Scalfani 的 Goodbye, Object Oriented Programming 我从事面向对象语言的编程已有数十年了。我使用的第一种面向对象语言是 C++,然后是 Smalltalk,最后是 .NET 和 Java。 我喜欢继承、封装和多态性的带来的诸多好处。这是面向对象范式的三个支柱。 我曾经渴望"重用",并在这个新的令人...

zsxjtip的博客 251

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

再见了,面向对象编程

为什么80%的码农都做不了架构师?>>> ...

weixin_34258782的博客 218
上一篇: 你可能还不知道,这5种动态炫酷图,其实也是用Python画的
下一篇: 升级版,用Python来进行多条曲线动态演示全球疫情变化
Wang_AI
博客等级 码龄12年 991粉丝 162原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值