首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

面向对象编程,再见!

  • 24-03-05 00:01
  • 2124
  • 9032
blog.csdn.net

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

640?wx_fmt=png

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

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

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

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

然而我大错特错了。

1.继承,倒塌的第一根支柱

640?wx_fmt=png

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

640?wx_fmt=png

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

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

香蕉猴子丛林问题

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

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

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

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

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

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

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

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

唉……

Erlang 的创建者 JoeArmstrong 有句名言:

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

香蕉猴子丛林的解决方法

这个问题的解决方法是,不要把类层次建得那么深。但如果继承是重用的关键,那么给继承机制添加的任何限制都会限制重用。对吧?

没错。

那我们可怜的面向对象程序员该怎么办?指望一杯三聚氰胺奶维系我们的健康吗?

答案就是:包含和委托(Contain and Delegate)。一会儿会详细解释。

菱形继承问题

早晚你会遇到下面这种恶心的问题,有些语言甚至根本解决不了。

640?wx_fmt=png

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

来看看下面的伪代码:

  1. Class PoweredDevice {
  2. }
  3. Class Scanner inherits from PoweredDevice {
  4.   function start() {
  5.   }
  6. }
  7. Class Printer inherits from PoweredDevice {
  8.   function start() {
  9.   }
  10. }
  11. Class Copier inherits from Scanner, Printer {
  12. }

注意 Scanner 和 Printer 类都实现了名为 start 方法。

那么问题来了,Copier继承哪个start?是Scanner的还是Printer的?肯定不可能同时继承啊。

菱形继承的解决

解决方案很简单:不要这样做。

没错。大多数面向对象都不让你这么干。

但是,但是……要是必须这样建模该怎么办?我需要重用!

那就必须使用包含和委托。

  1. Class PoweredDevice {
  2. }
  3. Class Scanner inherits from PoweredDevice {
  4.   function start() {
  5.   }
  6. }
  7. Class Printer inherits from PoweredDevice {
  8.   function start() {
  9.   }
  10. }
  11. Class Copier {
  12.   Scanner scanner
  13.   Printer printer
  14.   function start() {
  15.     printer.start()
  16.   }
  17. }

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

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

脆弱的基类问题

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

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

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

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

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

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

原来是这样……

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

  1. import java.util.ArrayList;
  2. public class Array
  3. {
  4.   private ArrayList<Object> a = new ArrayList<Object>();
  5.   public void add(Object element)
  6.   {
  7.     a.add(element);
  8.   }
  9.   public void addAll(Object elements[])
  10.   {
  11.     for (int i = 0; i < elements.length; ++i)
  12.       a.add(elements[i]); // this line is going to be changed
  13.   }
  14. }

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

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

下面是继承的类:

  1. public class ArrayCount extends Array
  2. {
  3.   private int count = 0;
  4.   @Override
  5.   public void add(Object element)
  6.   {
  7.     super.add(element);
  8.     ++count;
  9.   }
  10.   @Override
  11.   public void addAll(Object elements[])
  12.   {
  13.     super.addAll(elements);
  14.     count += elements.length;
  15.   }
  16. }

ArrayCount类是通用的Array类的特化。两者行为上的唯一区别就是ArrayCount会维护一个count,记录元素的个数。

我们来仔细看看这两个类。

Array的add()给局部的ArrayList添加一个元素。

Array的addAll()针对每个元素调用局部的ArrayList的add方法。

ArrayCount的add()调用父类的add()然后增加count。

ArrayCount的addAll()调用父类的addAll()然后给count增加相当于元素个数的数。

一切都很正常。

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

  1. public void addAll(Object elements[])
  2.   {
  3.     for (int i = 0; i < elements.length; ++i)
  4.       add(elements[i]); // this line was changed
  5.   }

从基类的作者的角度来看,这个类实现的功能完全没有变化。而且所有自动化测试也都通过来了。

但是基类的作者忘记了继承的类。而继承类的作者被错误吵醒了。

现在ArrayCount的addAll()调用父类的addAll(),后者在内部调用add(),而add()被继承类重载了。

因此,每次继承类的add()被调用时,count都会增加,然后在继承类的addAll()被调用时再次增加。

count被增加了两次。

既然会发生这种现象,那么继承类的作者必须清楚基类是怎样实现的。而且,基类的每个改动必须要通知所有继承类的作者,因为这些改动可能会以不可预知的方式破坏继承类。

唉!这个巨大的裂隙威胁到了整个继承支柱的稳定。

脆弱的基类的解决方法

这个问题还得要包含和委托来解决。

使用包含和委托,可以从白盒编程转到黑盒编程。白盒编程的意思是说,写继承类时必须要了解基类的实现。

而黑盒编程可以完全无视基类的实现,因为不可能通过重载函数的方式向基类注入代码。只需要关注接口即可。

这种趋势太讨厌了……

继承本应带来最好用的重用。

在面向对象语言中实现包含和委托并不容易。它们是为了继承方便而设计的。

如果你和我一样,你就会开始反思这个继承了。但更重要的是,这些问题应当引起你对于通过层次结构进行分类的反思。

层次结构的问题

每到一个新公司时,我都要为在哪儿保存公司文档(即员工手册)而纠结。

是应该建一个Documents文件夹,然后在里面建个Company呢?

还是应该建个Company文件夹,然后在里面建个Documents呢?

两者都可以。但哪个是正确的?哪个更好?

层次分类的思想是因为基类(父类)更通用,继承类(子类)更专用。沿着继承链越往下走,概念就越专用(见上面的形状层次)。

但如果父节点和子节点能随意交换位置,那么显然这种模型是有问题的。

层次结构的解决

真正的问题出在……

层次分类是错误的。

那层次分类应该用在哪里?

包含关系。

真实世界里有很多包含关系(或者叫做独占关系)的层次结构。

但你找不到层次分类。仔细想一下。面向对象范式是根据充满了各种对象的真实世界建立的。但它用错了模型——层次分类在真实世界中没有类比。

但真实世界里到处都是层次包含关系。层次包含关系的一个非常好的例子就是你的袜子。袜子放在装袜子的抽屉里,然后抽屉包含在衣柜里,衣柜包含在卧室里,卧室包含在房子里,等等。 

硬盘上的目录也是层次包含关系的另一个例子——它们包含文件。

那我们该怎样分类呢?

仔细想一下公司文档,就会发现其实放在哪儿都无所谓。我可以放在Documents目录下或者放在Stuff目录下也可以。

我选择的分类法是标签。我给它加上不同的标签。

  1. Document
  2. Company
  3. Handbook

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

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

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

再见,继承。

2.封装,倒塌的第二根支柱 

640?wx_fmt=png

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

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

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

封装是变量的保险柜。

封装太伟大了!

封装万岁…… 

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

引用问题

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

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

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

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

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

引用的解决

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

完全没有效率。

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

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

再见,封装。

3.多态,倒塌的第三根支柱

640?wx_fmt=png

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

就像是三人组中的Larry Fine。

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

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

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

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

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

4.破碎的承诺

640?wx_fmt=png

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

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

然后我发现被骗了。

再见,面向对象编程。

5.那该怎么办?

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

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

一朝被蛇咬十年怕井绳。

你懂的。

原文:https://medium.com/@cscalfani/goodbye-object-oriented-programming-a59cda4c0e53

作者:Charles Scalfani。

译者:弯月,责编:屠敏 

CSDN
微信公众号
成就一亿技术人
注:本文转载自blog.csdn.net的CSDN资讯的文章"https://blog.csdn.net/csdnnews/article/details/82975991"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top