几十年来,我一直在使用面向对象编程。我使用但第一个面向对象语言是C++,然后是Smalltalk,最后是.NET和Java。
我非常热衷于继承、封装、多态的优点。它们是范式的三大支柱。
我渴望获得重用的机会,并利用那些在我面前的人在这个新的、令人兴奋的风景中获得的智慧。
我一想到把真实世界的对象映射到类中,我就控制不住的兴奋,期望整个世界都可以找到一个合适的位置。
继承,第一支柱
乍一看,继承是面向对象范例中最好的特性。作为新灌输的例子,形状层次结构的所有简单例子似乎具有逻辑意义。
重用是今天的关键词。不,是这一年中甚至更久远的关键词。
我用新兴的眼光了解了这一切,并冲向世界。
香蕉猴丛林问题(你想要的是香蕉,但通常是一只大猩猩举着香蕉)
随着我心中的宗教和问题的解决,我开始构建类层次结构和编写代码,全世界都是对的。
我永远不会忘记我准备兑现承诺,重用现有的继承类的那天,这是我一直在等待的时刻。
一个新的项目过来,我回想起我非常喜欢的上一个项目中有这样的类可以用。
没问题,重用可以这样做。我要做的很简单就是从其他项目里抓取这个类并使用它。
哇,好吧。不仅仅是这个类,我们还需要抓取它的父类。
等下,我们好像还需要抓取它的父类的父类……然后我们需要整个父类……好吧好吧,我处理了这个问题。
漂亮!现在它不能编译了,为什么呢?这个对象包含着其他对象,所以我还需要抓取包含着的对象。
等一下,我不仅仅需要包含的对象,我还需要包含的对象的父类,直到包含了用到的每一个对象。并且所有的父类中还有它们的父类的父类……
下面是饮用Joe的一句话,Erlang的开创者:
香蕉猴丛林问题的解决办法
我可以通过不创造那么多深层次的继承类解决这个问题。但是如果继承是重用的关键,那么我在这个机制上的任何对重用的限制都会限制重用的作用。
一个多么可怜的程序员,谁可以有一个不错的办法帮助他呢?
钻石问题
迟早,下面的问题会变得丑陋,取决于使用的语言,无法解决的头脑。
大多数面向对象语言都不支持这个,尽管似乎是符合逻辑的。在面向对象语言中支持这个到底有什么困难?
看一下下面的伪码:
注意一下Scanner和Printer类都实现了一个start方法。
所以到底哪个方法是Copier类去继承的?Scanner?Printer?都不是。
钻石问题的解决办法
解决问题的办法也很简答,就是不要那么做。
但是,如果我有这样的model呢,我想要重用!
然后你必须包含和委派
这里注意Copier类现在包含了Printer类和Scanner类的实例。Copier委派Printer类的start方法去实现自己的start方法,它也可以简单的委派给Scanner类的start方法。
易碎的Base类问题
所以我的层次结构做的很浅,使它们保持周期性,这样我就没有了钻石问题。
有一次,我的代码第二天挂了。我没有改动我的代码。
喔,它可能是一个bug…等等…有些东西改动了…
但不是在我的代码里改动的。原来是我继承的类改动了。
基类的改动怎么让我的代码挂了呢?
看一下下面的基类:
注意被注释的那一行,那一行改动会让我的代码挂掉。
下面是派生类
Array的add()添加一个元素到本地的ArrayList
Array的addAll()调用本地的ArrayList的add()方法添加循环的每一个元素。
ArrayCount的add()方法调用了父类的add()然后count变量+1。
ArrayCount的addAll()方法调用父类的addAll()然后加上元素数组的长度。
现在做突破性改变,备注是的那一行代码被改成如下
就基类的所有者而言,它仍像广告一样起作用,并且所有的自动化测试也能通过。
但是显然所有者忽略了派生类。派生类的所有者处在错误的觉醒中。
现在ArrayCount的addAll()调用它父类的addAll(),内部调用add()由派生类复写了。
这样就造成了每次调用派生类的add()都会使得count+1,然后通过派生类的addAll()又被加了一次。addAll()会跳用派生类复写的add()方法。
这样会增加两次。
如果这可能发生,并且确实发生了。派生类的作者必须知道基类是如何实现的。在基类上做的每一个改动都需要通知他们因为这个基类有可能导致他们的派生类以不可预测的方式发生中断。
这个巨大的裂缝永远威胁着宝贵的继承支柱的稳定性。
不稳定的基类问题的解决办法
再次包含和委派来拯救这个问题。
通过使用包含和委派机制,我们可以从白盒编程进入到黑盒编程。使用白盒编程的话,我们必须查看一下基类的实现。
使用黑盒编程的话,我们可能会完全忽视掉基类的实现因为我们没有通过覆盖某个方法的方式注入代码到基类中。我们仅仅需要关心我们自己的接口。
这个趋势令人不安…
继承被认为是重用的巨大成果。
面向对象语言不是简单的包含与委派。他们被设计出来去使得继承简单。
如果你和我一样,你开始担心继承的这类问题。但是更重要的是,它会动摇你层次结构划分的自信心。
层次结构问题
每次我从一家新公司开始,在我创建一个类放到我公司文档的时候,我就在努力解决这个问题。例如员工手册。
我创建了一个Documents文件夹,然后在里面创建了Company文件夹?或者我在同级目录创建了一个Company文件夹并且创建一个Documents文件夹。
这两个方式都行,但哪一个是正确的?哪一个更好呢?
分类层次结构的思想是有很多父类设计一般的方法然后派生类是更加专业化的版本,甚至更更专业化因为我们走了继承链。
但是如果父类和子类可以随意切换位置,那么显然这个模型是有问题的。
分类层次不起作用,那么继承还有什么好处?
包含。
你看一下世界的整个网络,你会发现到处包含层次结构(或独占所有权)。
你找不到等级结构,让我们沉思片刻。面向对象的范例是基于充满对象的真实世界改造的。但是它使用了破碎的模型。即,分类层次,没有真实世界的类比。
但现实世界充满了包含层次。包含层次一个很好的例子就是你的袜子。它们在一个抽屉里,它被包含在装着你衣服的大抽屉里,而这个大抽屉被包含在你的房子的卧室中,等等。
你硬盘驱动里的目录是另一个包含层次的例子,它们包含着文件。
那么我们怎么样它们分类呢?
我通过标签给目录分类。我标记文件如以下的标签:
标签没有顺序或者层次(这样也可以解决钻石问题)。
标签和接口是类似的,因为你可以有多种相关文件的类型。
但是有太多的裂缝,看起来继承支柱倒下了。
封装,第二大支柱
初探,封装是面向对象编程第二大支柱。
对象状态变量受外部访问的保护,即它们被封装在对象中。
我们不再需要担心谁知道谁正在访问的全局变量。
封装安全服务于你的变量。
长久使用封装,直到…
引用问题
出于高效的缘由,对象被传递到方法中,不是通过它们的值,而是通过引用。
这意味着函数不再传递对象,而是传递引用或者对象的指针。
如果一个对象通过引用被传递到构造器对象,那么这个构造器就可以把这个传递的引用作为自己的通过封装保护的私有变量。
但是传递对象是不安全的!
为什么呢?因为另一段代码指向对象的指针,即调用构造函数的代码。它必须有对对象的引用,否则它不能传递参数给构造函数?
引用问题的解决
构造器必须克隆一份被传递的对象,不是浅克隆而是深克隆。每个对象都包含传递的对象和他们自己的这些对象。
效率如此之高。
这里有个劫,不是每个对象都可以被克隆。一些拥有与它们相关的操作系统资源,使得克隆在最好或最坏的情况下是无用的。
每个主流的面向对象语言都有这个问题。
多态,第三个支柱
多态是面向对象三位一体的红发继子。(垃圾堆捡的?)
它是团队的Larry Fine。
哪里都有它们的影子,但是他都是这个特征。
只支持字符并不是说多态性不好用,只是你不需要一个面向对象语言来获得这个。
接口将会提供你这些,并没有所有的OO箱子?
有了这些接口,你可以无限制的混入很多不同的行为。
因此,我们不必太多的向面向对象多态性告别,并且可以向基于接口的多态性示好。
破碎的诺言
面向对象在早期承诺了太多东西,这些承诺仍然是对那些坐在教室里,阅读博客和在线课程的天真的程序员们做出的。
我花了好几年才意识到面向对象欺骗了我,我也睁大眼睛,缺乏经验和信任。
88,面向对象程序。
那又怎么样?
哈喽,函数式编程。过去几年和你一起工作真的是太好了。
就如你所看到的,我不会接受你的任何承诺。我的了解它然后才去信任它。
原文链接:https://medium.com/@cscalfani/goodbye-object-oriented-programming-a59cda4c0e53