再见了,面向对象编程

穿精又带淫゛_ 提交于 2020-04-18 05:53:51

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

这是一篇长文,共有三大块,我先翻译第一块,以后有时间更新后面的

我已经用面向对象编程了好几十年了,从最开始的C++然后到Smalltalk最后到.NET和JAVA。我一直期待能充分利用面向对象编程的三大核心:继承、封装、多态带来的好处。我渴望从代码复用这一效果中能窥探到前辈们的智慧。想象着把真实世界映射成代码里各种类,我简直不能太兴奋,还满怀期待的等着他们像真实世界一样正常运转起来。

这样想就大错特错了。

继承,第一个失败的核心

乍一看,继承显然应该是面向对象编程最大的好处。对于刚刚接触面向对象思想的人来说,这些简洁的层级形状示例看起来非常有道理。


而“代码复用”,多年来都是面向对象思想的代名词。我毫不犹豫的接受了这种思想,并且一头扎进刚刚发现的这片新大陆。

“香蕉猴子雨林” 问题


成为面向对象虔诚信徒的我,带着手头的问题,开始构建类层次并编码。一切都还好。
然而,我永远忘不了当我准备利用继承来使用已有类库的那一天,毕竟说好的复用效果就要出现了。我™为这天可等了不少时候了。

一个新项目交到我的手上,我又想起来在自己上个项目中颇为喜爱的一个类。
没问题,复用拯救世界。我只需要把那个类从上一个项目拽出来放到新的项目里就万事大吉啦。
嗯,看起来好像不只需要这一个类。我们还需要这个类的父类。不过,不过,唉,就这样吧。
额,等等,还需要父类的父类,接着还要所有的父类。行行行,我能办到,没问题。

真是太好,现在™编译不过去了。什么鬼?哦,明白了,这个对象包含其他对象,没问题我们连这个对象一起包含进来。
我去,不是吧,还要这个对象的父类,不光是它,所有包含的对象的父类,以及父类的父类。我的天哪。

Erlang之父Joe Armstrong曾经说过:面向对象编程语言的问题在于,冰山一角下暗藏的各种依赖成了他们的负担。你仅仅想要个香蕉,但你不得不连拿着香蕉的大猩猩,以及大猩猩所在的雨林一起拿走。

“香蕉猴子雨林” 问题的解决方案


我可以通过不构建过深的层级结构来解决这个问题。但是,如果说继承是复用的关键机制,如果继承的从深度不够,那同样也会限制复用的效果,不是吗?
没错。
那么那些面向对象编程的脑残粉程序员该怎么办呢?
包含并且委托。这一方法以后会经常见到。

钻石问题

或早或晚,以下问题都会揭露出面向对象编程的丑陋之处——分不清从哪个开始

大多数面向对象编程语言都不支持这种结构,虽然逻辑上并没有问题。那么在面向对象编程语言中支持这种结构有那么难吗?

来看看下面的伪代码:

Paste_Image.png

注意到扫描仪类和打印机类都实现了一个叫做“开始”的方法吗?
所以复印机到底继承了哪个类里面的“开始”方法?扫描仪的还是打印机的?肯定不能是两个都继承了吧。

钻石问题解决方案


答案很简单,不要这么用。
没错,大多数面向对象编程语言不会让你这么用的。
但是,如果我非要这样建模呢?我想复用啊!
那你必须,包含并且委托。

Paste_Image.png

看到了吗,现在复印机类包含了一个打印机的实例和一个扫描仪的实例。它把“开始”方法委托给了打印机类去实现。当然,你也可以轻易的把它委托给复印机类。
这是继承这一核心特性的又一瑕疵。

脆弱基类问题


所以我尽量使我的层级足够浅,而且避免循环继承。嗯这下没有钻石问题了。

嗯,世界还是那么美好一切运行良好,直到。。。
一天,我的代码运转正常,但第二天去停止工作了。那么问题来了,我™没改过代码啊!

嗯,也许是出bug了,等等,不对,确实有什么地方更改了。
但改动的部分不是我的代码。是我继承的基类代码改了。

为毛基类的变化会使我的代码崩溃?
原因在于。。。
想象一下下面的基类(虽然是用JAVA写的,但即使你不是JAVA程序员应该也很容易理解)

Paste_Image.png

看到注释那行代码了吗,就是这行代码的改变影响了我的代码。

这个类有两个接口类:add()和addAll()。add()方法会向数组中添加一个元素,而addAll()方法会通过调用add()方法来把多个元素添加到数组里。
下面是继承类

Paste_Image.png

ArrayCount类是基类Array的具体化。ArrayCount与Array的唯一区别是,ArrayCount会在添加了元素以后再计算一下元素的个数。

让我们来仔细看看这两个类。
基类Array的add方法把一个元素添加到一个本地数组中。基类Array的addAll方法调用本地数组的add方法来把每个元素添加到数组中。

ArrayCount类的add方法调用父类的add方法,并且把count的数目自增1。ArrayCount类的addAll方法调用父类的addAll方法并且把count加上元素的个数。
一开始都没有问题。

 

现在到了关键的一行代码了!注释的这行基类的代码改成了下面这样:

Paste_Image.png

只要考虑到基类的拥有者,方法还是能正常运行。所有的自动测试也能通过。但继承类并不知道是哪个拥有者,是基类的拥有者,还是继承类的拥有者,于是继承类的拥有者被粗鲁的唤醒了。

现在,ArrayCount的addAll方法调用了他的父类中的addAll方法。父类内部会调用已经被继承类覆盖的add方法。会导致循环添加元素的时候,每次循环都让count自增了1,最后又会让count增加上元素的个数。这™就尴尬了。丫算了两次!

这种情况确实存在,继承类的编写者必须清楚基类是如何实现内部的方法的。并且基类有任何变化,必须都同步给他,要不他的继承类会以不可预知的方式崩溃!

啊!这个巨大的漏洞成了悬在继承这一核心特性头上的达摩克利斯之剑,随时威胁着我们所珍视的继承特性的稳定性。

脆弱基类的解决方案


再一次祭出包含和委托大法吧。

通过使用包含和委托,我们从白盒编程(这个不知道的小白同学请自行百度,还有灰盒,国内一般测试用这种说法比较多)变成了黑盒编程。白盒编程,我们必须了解基类的实现过程。

而黑盒编程,因为我们不能通过覆盖基类的方法来改写基类的代码。我们能完全忽略基类的实现过程。我们只需要了解接口就好了。

这个走向有点令人困惑啊。

继承本来是应该实现复用的。
但是面向对象编程并没有使得包含和委托这种繁琐操作更加加单。反而是包含和委托使得继承用起来容易些。。。

如果你跟我的经历差不多,你估计也开始重新思考继承这个玩意了。但最重要的,这应该打击到你对分层分类的信心了。

分层问题

每次我进入一家新公司,我老是纠结怎么给我的公司文档建立目录结构。比如说员工手册。我是应该建立一个叫做文档的文件夹,然后在里面建立一个叫做公司的文件夹?还是应该建立一个叫做公司的文件夹,然后在里面建立一个叫做文档的文件夹?

两种都可以,但是那种是对的?哪种更好?

分类分层的假设是说,存在着一些基类(父类),他们是最基础的,然后他们的继承类(子类)会在他们的基础上更加具体化。而且层级越往下走,就应该越具体。(看看上面的层级形状示例)

但如果父类和子类不断的调换位置,那这个模型很明显有问题。

分层问题的解决方案


麻蛋
分类分层就没有个卵用
那分层到底有什么好处?

包含


如果你看看真实世界,你会发现包含层级(专有)几乎无处不在

但你在真实世界里找不到叫做分类分层的东西。我们先把它放在一边。面向对象范式是以充满对象的真实世界为基础的。但却使用了一个有问题的模型,也就是真实世界里根本找不到的分类分层。

但真实世界确实充满了包含层级。包含层级的一个不错的例子就是你的袜子。你的袜子放在抽屉里,而抽屉是在衣柜里,衣柜又在你的卧室里,卧室又在你的房子里,等等。

你硬盘上的目录也是包含层级的一个例子。他们包含着文件

那我们我们该如何分类?

哈,如果你还想着公司文档,其实我怎么分类都可以。我可以把他们放在叫做文档的文件夹里或者叫做资料的文件夹里。

我分类的方式是贴标签,我是这么给文件贴标签的:
文档公司手册
标签没有顺序或者层级(这下也把钻石问题解决了)
标签类似于接口,你可以给文件关联上多种类型。
但是有这么多毛病的继承核心特性,看起来失败了。
再见吧,继承!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!