Loading [MathJax]/jax/input/TeX/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >面向对象编程,再见!

面向对象编程,再见!

原创
作者头像
JAVA高级架构开发
修改于 2018-10-09 01:59:12
修改于 2018-10-09 01:59:12
1.2K0
举报

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

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

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

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

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

然而我大错特错了。

说到这里,也给大家推荐一个架构交流学习群:835544715,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,相信对于已经工作和遇到技术瓶颈的码友,在这个群里会有你需要的内容。

继承,倒塌的第一根支柱

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

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

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

香蕉猴子丛林问题

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

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

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

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

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

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

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

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

唉……

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{

Scanner scanner

Printer printer

functionstart(){

printer.start()

}

}

注意现在 Copier 类包含一个 Printer 实例和一个 Scanner 实例。然后将 start 函数委托给 Printer 类的实现。要委托给 Scanner 也很简单。

这个问题是继承这根支柱上的另一条裂缝。

脆弱的基类问题

好吧,那我尽量使用较浅的类层次结构,并保证里面没有环,这样就不会出现菱形继承了。

似乎一切都解决了。直到我们发现……

我前一天工作得好好的代码今天出错了!关键是,我没有改任何代码!

嗯也许是个 bug……但等等……的确有些改动……

但改动的不是我的代码。似乎改动来自我继承的那个类。

为什么基类的改动会破坏我的代码?

原来是这样……

看看下面这个基类(用Java写的,但就算你不懂Java,应该也很容易看懂):

import java.util.ArrayList;

publicclassArray

{

privateArrayList a =newArrayList();

publicvoidadd(Object element)

{

a.add(element);

}

publicvoidaddAll(Object elements[])

{

for(inti =0; i < elements.length; ++i)

a.add(elements[i]);// this line is going to be changed

}

}

重要提示:注意加了注释的那一行。稍后这行的改动将会导致别的东西出错。 

这个类的接口上有两个函数:add() 和 addAll()。add() 函数负责添加一个元素,addAll() 函数会调用 add 函数添加多个元素。 

下面是继承的类:

publicclassArrayCountextendsArray

{

privateintcount =0;

@Override

publicvoidadd(Object element)

{

super.add(element);

++count;

}

@Override

publicvoidaddAll(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增加相当于元素个数的数。

一切都很正常。

现在是出问题的地方。基类中加注释的那行代码现在改成这样:

publicvoidaddAll(Object elements[])

{

for(inti =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

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

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

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

再见,继承。

封装,倒塌的第二根支柱 

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

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

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

封装是变量的保险柜。

封装太伟大了!

封装万岁…… 

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

引用问题

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

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

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

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

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

引用的解决

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

完全没有效率。

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

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

再见,封装。

多态,倒塌的第三根支柱

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

就像是三人组中的Larry Fine。

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

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

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

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

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

破碎的承诺

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

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

然后我发现被骗了。

再见,面向对象编程。

那该怎么办?

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

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

一朝被蛇咬十年怕井绳。

你懂的。

想要学习Java高架构、分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频免费获取  架构群:835544715

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
面向对象编程会被抛弃吗?这五大问题不容忽视
20 世纪 60 年代,编程遇到了一个大问题:计算机还没有那么强大,需要以某种方式平衡数据结构和程序之间的能力。
机器之心
2020/10/27
5200
面向对象编程会被抛弃吗?这五大问题不容忽视
【JAVA-Day62】Java继承:面向对象编程中重要的基石
本技术博客将深入研究Java中继承的核心概念,探寻其在面向对象编程中的重要地位。通过对继承的本质、倡导原因、技巧、与封装、多态的融合,以及面试考点的深入理解,我们将为你呈现继承的全貌,揭示其在实际开发中的价值和实践总结。
默 语
2024/11/20
1990
【JAVA-Day62】Java继承:面向对象编程中重要的基石
【深入浅出C#】章节 4: 面向对象编程基础:封装、继承和多态
封装、继承和多态是面向对象编程中的核心概念,它们对于构建灵活、可扩展和可维护的软件系统至关重要。 封装(Encapsulation)通过将数据和相关操作封装在一个类中,隐藏内部实现细节,并提供公共接口来与外部进行交互。封装有助于保护数据的完整性和安全性,同时提供了良好的抽象,使得代码更易于理解和使用。封装还可以支持代码的模块化和团队开发,各个模块之间可以独立开发和测试,提高了代码的可维护性和复用性。 继承(Inheritance)允许一个类继承另一个类的属性和方法,从而实现代码的重用和扩展。继承提供了代码的层次结构,使得相关的类可以组织在一起,并且可以通过继承实现代码的共享和统一的接口。继承还可以支持多态性,通过在子类中重写父类的方法,实现不同对象的不同行为。 多态(Polymorphism)允许同一操作在不同的对象上产生不同的行为。多态性提供了灵活性和扩展性,使得代码可以处理多种类型的对象,而不需要显式地针对每种类型编写不同的代码。多态性可以通过方法重写、方法重载和接口的使用来实现,它可以使代码更加灵活和可扩展,同时提高了代码的可读性和可维护性。
喵叔
2023/07/09
7890
使用 JavaScript 理解面向对象编程的四大支柱
面向对象编程是一种编程范式,它使您能够使用对象和类对代码进行建模和结构化。虽然JavaScript不是一门完全面向对象的语言,但您仍然可以利用面向对象编程的核心原则编写更清晰、更易维护的代码。面向对象编程有四个主要支柱:
泽霖
2023/11/11
2600
比较分析C++、Java、Python、R语言的面向对象特征,这些特征如何实现的?有什么相同点?
–  比较分析C++、Java、Python、R语言的面向对象特征,这些特征如何实现的?有什么相同点?
西湖醋鱼
2020/12/30
1.8K0
Rust学习笔记之面向对象编程
今天,我们继续「Rust学习笔记」的探索。我们来谈谈关于「面向对象编程」的相关知识点。
前端柒八九
2023/08/01
2820
Rust学习笔记之面向对象编程
Python面向对象编程
OOP,即面向对象编程(或 “面向对象程序设计” ,Object Oriented Programming)。类和对象是OOP中的两个关键内容,在面向对象编程中,以类来构造现实世界中的事物情景,再基于类创建对象来进一步认识、理解、刻画。根据类来创建的对象,每个对象都会自动带有类的属性和特点,然后可以按照实际需要赋予每个对象特有的属性,这个过程被称为类的实例化。
Francek Chen
2025/01/22
1110
Python面向对象编程
【JavaSE专栏62】继承,JAVA面向对象编程中的一项重要特性
本文讲解了 Java 中面向对象继承的概念及语法,并给出了样例代码。继承是面向对象编程中的一项重要特性,它允许一个类继承另一个类的属性和方法。
Designer 小郑
2023/08/02
3630
【JavaSE专栏62】继承,JAVA面向对象编程中的一项重要特性
08.面向对象的特性
提到面向对象,相信很多人都不陌生,随口都可以说出面向对象的四大特性:封装、抽象、继承、多态。
杨充
2025/03/24
1350
谈谈面向对象编程
最近写了些和函数式编程的文章,有读者和我讨论函数式编程和面向对象编程的优劣。二者都是很好的编程思想,都在着力解决代码重用的问题,也彼此吸收对方的优点,所以大可不必去分个高下。然而, 我面试过许多号称精通面向对象编程(比如:Python / Ruby / C++)的工程师,随便问几个问题,就可以看出这个人对面向对象的理解: 你觉得在面向对象编程中,最重要的思想是什么? 如果有人提及「继承」,我会让她写个她在工作中使用继承的例子。 如果有人提及「多态」,我会让她解释一下多态,并让她写个她在工作中使用多态的例子。
tyrchen
2018/03/28
9290
谈谈面向对象编程
什么是面向对象编程?OOP 深入解释
面向对象编程 (OOP) 是一种基本的编程范式,几乎每个开发人员都在其职业生涯的某个阶段使用过。OOP 是用于软件开发的最流行的编程范例,并且在大多数程序员的教育生涯中被作为标准编码方式教授。 另一种流行的编程范式是函数式编程,但我们现在不讨论它。
用户4235284
2023/10/14
1.9K0
什么是面向对象编程?OOP 深入解释
【Python】教你彻底认识Python中的面向对象编程
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它通过“对象”将数据和方法进行封装,以提高代码的可重用性和可维护性。Python是一种多范式的编程语言,它不仅支持面向过程编程,也支持面向对象编程。在这篇文章中,我们将深入探讨Python中的面向对象编程,包括其基本概念、类和对象的创建、继承、多态,以及一些实际应用示例。
E绵绵
2025/05/25
1650
面向对象编程
面向对象提供的基本机制,对于提高开发、沟通等各方面效率至关重要。考察面向对象也是面试中的常见一环,下面我来聊聊面向对象设计基础。
希望@蓝
2023/12/08
3202
从结构化过程式编程到面向对象编程:一个平稳的过渡
面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,它使用“对象”来设计软件。对象是包含数据(也被称为属性)和操作这些数据的方法的实体。面向对象编程的主要目标是提高软件的可重用性、灵活性和可维护性。
运维开发王义杰
2023/08/10
3740
从结构化过程式编程到面向对象编程:一个平稳的过渡
Python教程(21)——面向对象编程的三大特性
在Python中,面向对象编程是一种核心的编程思想。Python被称为“一切皆对象”的语言,因为在Python中,几乎所有的数据都被视为对象。这包括数字、字符串、列表、函数等基本类型,还有自定义的类和对象。
一点sir
2024/01/10
1620
Python教程(21)——面向对象编程的三大特性
Python 面向对象编程详解
Python 支持面向过程、面向对象、函数式编程等多种编程范式,且不强制我们使用任何一种编程范式,我们可以使用过程式编程编写任何程序,在编写小程序时,基本上不会有问题.但对于中等和大型项目来说,面向对象将给我们带来很多优势.接下来将结合面向对象的基本概念和Python语法的特性讲解面向对象的编程.
王 瑞
2022/12/28
6460
【深入浅出C#】章节 4: 面向对象编程基础:类和对象的概念
类和对象是面向对象编程中最基本的概念,它们在程序设计中起着重要的作用。类是一种抽象的数据类型,用于描述具有相似属性和行为的一组对象。对象则是类的实例,代表了现实世界中的具体事物或概念。 面向对象编程的核心思想是将现实世界的事物抽象成类,通过创建对象来模拟和处理问题。类和对象的概念使得程序能够更加模块化、可维护和可扩展。下面是类和对象在面向对象编程中的重要性:
喵叔
2023/07/09
5320
如何给6岁小朋友讲解面向对象编程
尽管这些问题可能微不足道,但它们很重要,因为它们提供了关于你的线索。你现在的心态,态度,观点。
AiTechYun
2019/07/22
1K0
面向对象的三个基本特征
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
牛老师讲GIS
2018/10/23
10.9K0
面向对象的三个基本特征
面向对象编程的理解
面向着具体的每一个步骤和过程,把每一个步骤和过程完成,然后由这些功能方法相互调用,完成需求。
Abalone
2022/07/14
3640
推荐阅读
相关推荐
面向对象编程会被抛弃吗?这五大问题不容忽视
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档