再见,面向对象编程
我已经用面向对象语言编程几十年了。我使用的第一个OO语言是c++,然后是Smalltalk,最后是。net和Java。
我很想利用继承,封装,多态性.范式的三大支柱。
我渴望获得重用的承诺,并利用我的前辈在这个新的和令人兴奋的领域中获得的智慧。
一想到要把现实世界的对象映射到它们的类中,我就无法抑制自己的兴奋,并期望整个世界都能井然有序地就位。
我大错特错了。
遗产,第一根倒下的支柱
乍一看,继承似乎是面向对象范式的最大好处。所有简单的形状层次的例子,都被作为例子展示给新灌输的人,似乎有逻辑意义。
重用是今天的关键词。不,让它成为一年,也许永远。
我吞下了这一切,带着我新发现的洞察力冲进了这个世界。
香蕉猴子丛林问题
带着心中的宗教信仰和需要解决的问题,我开始构建Class Hierarchies并编写代码。整个世界都很美好。
我永远不会忘记那天,我准备通过继承现有类来兑现重用的承诺。这是我一直在等待的时刻。
一个新的项目出现了,我回想起了我在上一个项目中非常喜欢的课程。
没有问题。重用来拯救。我要做的只是从其他项目中抓取类并使用它。
嗯,实际上,不只是那个班。我们需要父类。但是,就这样了。
等等,看来我们还需要父母的父母…然后,我们需要所有的家长。好吧,我来处理。没有问题。
和伟大的。现在它不会编译了。为什么? ?哦,我知道了……这对象包含另一对象。所以我也需要这个。没有问题。
等等,我不只是需要那对象。我需要对象的父对象和父对象的父对象等等所有包含的对象的所有父对象以及它们的父对象,父对象,父对象。
啊。
有一句名言乔·阿姆斯特朗, Erlang的创造者:
面向对象语言的问题是它们有所有这些隐式环境。你想要一根香蕉,但你得到的是一只拿着香蕉的大猩猩和整个丛林。
香蕉猴子丛林解决方案
我可以通过不创建太深的等级制度来解决这个问题。但是如果继承是重用的关键,那么我对该机制的任何限制肯定会限制重用的好处。对吧?
正确的。
那么,一个可怜的面向对象程序员,一个健康的Kool-aid帮助,该怎么办呢?
包含和委托。稍后再详细介绍。
钻石的问题
根据语言的不同,下面的问题迟早会露出它丑陋的、无法解决的脑袋。
大多数OO语言不支持这一点,尽管如此似乎要有逻辑意义。在OO语言中支持这一点有什么困难?
想象一下下面的伪代码:
类PoweredDevice {
}类扫描器继承自PoweredDevice {
函数开始(){
}
}类Printer继承自PoweredDevice {
函数开始(){
}
}类copy继承自Scanner, Printer {
}
注意到扫描仪类和打印机类实现一个名为开始.
哪个start函数复印机类继承?的扫描仪一个?的打印机一个?不可能两者都是。
钻石的解决方案
解决办法很简单。不要这样做。
是的没错。大多数OO语言不允许您这样做。
但是,但是,如果我要做这个模型呢?我要我的重用!
那么你必须包含和委托.
类PoweredDevice {
}类扫描器继承自PoweredDevice {
函数开始(){
}
}类Printer继承自PoweredDevice {
函数开始(){
}
}类复印机{
扫描仪扫描
打印机的打印机
函数开始(){
printer.start ()
}
}
注意这里复印机类的实例打印机和一个扫描仪.它代表开始函数打印机类的实现。它可以很容易地委托给扫描仪.
这个问题是继承支柱的另一个裂缝。
脆弱的基类问题
所以我把我的层次结构做得很浅,避免它们是周期性的。不要给我钻石。
整个世界都很美好。直到……
一天,我的代码工作,第二天它停止工作。意外的是。我没有更改我的代码.
嗯,也许是一个bug,但是等等……做改变...
但我的代码里没有.结果发现变化发生在我继承的那个班级。
基类中的一个变化怎么可能破坏我的代码??
这就是……
想象一下下面的基类(它是用Java写的,但如果你不懂Java,应该很容易理解):
进口java.util.ArrayList;
公共类数组
{
private ArrayList
重要的注意注释的代码行。这一行稍后将被更改,这将破坏一些东西。
这个类的接口上有两个函数,add ()而且addAll ().的add ()函数将添加单个元素和addAll ()将添加多个元素通过调用add函数.
这里是派生类:
公共类ArrayCount扩展了Array
{
私有int count = 0;
@Override
添加(对象元素)
{
super.add(元素);
+ +计数;
}
@Override
addAll(对象元素[])
{
super.addAll(元素);
数+ = elements.length;
}
}
的ArrayCount类是专门化的一般数组类。的只有行为差异是ArrayCount保持一个数元素的个数。
让我们详细看看这两个类。
的数组add ()向局部变量添加一个元素ArrayList.
的数组addAll ()调用本地ArrayList为每个元素相加。
的ArrayCountadd ()调用它的父母的add ()然后递增数.
的ArrayCountaddAll ()调用它的父母的addAll ()然后递增数由元素的个数决定。
一切工作正常。
现在的断变化.基类中的注释行代码更改为如下:
addAll(对象元素[])
{
For (int I = 0;我< elements.length;+ + i)
添加(元素[我]);//这一行被更改了
}
就基类的所有者而言,它仍然像所宣传的那样工作。所有的自动化测试仍然通过.
但是所有者不知道派生类。派生类的所有者将猛然觉醒。
现在ArrayCount addAll ()调用它的父母的addAll ()哪一个在内部调用add ()已重载由派生的类。
这原因数每次递增派生的类的add ()是被调用的,然后是增加再一次元素的个数派生的类的addAll ().
这是计算两次。
如果发生了这种情况,派生类的作者必须知道基类是如何实现的。而且必须通知他们基类中的每一个更改,因为它可能以不可预测的方式破坏他们的派生类。
啊!这条巨大的裂缝永远威胁着珍贵的传承柱的稳定。
脆弱基类解决方案
再次进行遏制和委派救援。
通过使用包含和委托,我们从白盒编程过渡到黑盒编程。使用白盒编程,我们必须查看基类的实现。
使用黑盒编程,我们可以完全忽略实现,因为我们不能通过覆盖基类的一个函数来将代码注入基类。我们只需要关心界面。
这种趋势令人不安……
继承被认为是重用的巨大胜利。
面向对象语言并没有使包含和委托变得容易。设计它们是为了让继承变得容易。
如果你和我一样,你会开始对遗产这件事感到疑惑。但更重要的是,这将动摇您对通过层次结构进行分类的能力的信心。
层次的问题
每次我开始在一个新公司工作,当我在创建一个地方放置我的公司文件,例如员工手册的时候,我就会纠结于这个问题。
我是先创建一个名为Documents的文件夹,然后再创建一个名为Company的文件夹吗?
还是先创建一个名为Company的文件夹然后再创建一个名为Documents的文件夹?
这两个工作。但哪个是对的呢?哪个是最好的呢?
类别层次结构的思想是,有更一般的基类(父类),派生类(子类)是这些类的更专门的版本。当我们沿着遗传链往下走的时候,就更加专业化了。(见上面的形状层次结构)
但如果父母和孩子可以随意交换位置,那么这个模型显然有问题。
层次结构的解决方案
有什么问题是……
分类等级制度不起作用.
那么等级制度有什么好处呢?
容器.
如果您看看现实世界,您会看到随处可见的遏制(或独占所有权)层次结构。
你找不到的是分类层次。让我们仔细想想。面向对象范式是建立在现实世界之上的,一个充满了对象的世界。但它使用了一个破碎的模型,即分类层次,在那里没有现实世界的类比。
但现实世界充满了包容等级制度。包含层次的一个很好的例子就是你的袜子。它们在袜子抽屉里,在梳妆台抽屉里,在卧室抽屉里,在家里,等等。
硬盘驱动器上的目录是包含层次结构的另一个例子。它们包含文件。
那么我们如何分类呢?
嗯,如果你想到公司文件,我把它们放在哪里几乎不重要。我可以把它们放在“文档”文件夹或“东西”文件夹里。
我分类的方法是用标签。我用以下标记标记该文件:
文档
公司
手册
标签没有顺序或层次。(这也解决了钻石问题。)
标记类似于接口,因为可以有多个与文档相关联的类型。
但有这么多裂缝,看起来继承支柱已经倒塌了。
再见,继承。
封装,倒下的第二根支柱
乍一看,封装似乎是面向对象编程的第二大好处。
对象状态变量受到保护,不受外部访问,即它们被封装在对象中。
我们不再需要担心被未知的人访问的全局变量。
封装是变量的安全。
这个封装的东西是不可思议的!!
封装万岁……
直到……
引用的问题
为了提高效率,对象不是通过值而是通过引用传递给函数的。
这意味着函数不会传递Object,而是传递一个参考或指向对象的指针。
如果对象通过引用传递给对象构造函数,构造函数可以将该对象引用放在由封装保护的私有变量中。
但是,通过物体不安全!
为什么不呢?因为另一段代码有一个指向对象的指针,也就是调用构造函数的代码。它必须有一个对象的引用,否则它不能传递给构造函数?
参考解决方案
构造函数必须克隆传入的对象。不是浅克隆,而是深克隆,也就是说,传入的对象中包含的每个对象以及这些对象中的每个对象,等等。
效率就是这样。
这是最关键的。不是所有对象都可以克隆。有些具有与之关联的操作系统资源,使得克隆在最好的情况下是无用的,在最坏的情况下是不可能的。
和每一个单一的主流面向对象语言有这个问题。
再见,封装。
多态性,倒下的第三根支柱
多态性是面向对象三位一体的红头发继子。
他有点像我们组里的拉里·法恩。
无论他们去哪里,他都在,但他只是一个配角。
这并不是说多态性不好,只是你不需要一个面向对象的语言来实现它。
接口会给你这些。而且没有OO的所有包袱。
有了接口,你可以混合多少不同的行为是没有限制的。
所以不用多说,我们和面向对象多态性和你好基于接口的多态性。
破碎的承诺
OO在早期确实承诺了很多。这些承诺仍在向坐在教室里、读博客、上在线课程的天真程序员做出。
我花了很多年才意识到OO是怎么骗我的。我也睁大了眼睛,缺乏经验,轻信别人。
我被灼伤了。
再见,面向对象编程。
然后什么?
你好,函数式编程.在过去的几年里和你一起工作真是太好了。
你要知道,我不会把你的承诺当真的。我得亲眼看到才会相信。
一朝被蛇咬,十年怕井绳。
你理解。
如果你喜欢,请点击下面的链接,这样其他人就可以在Medium上看到它了。betway娱乐官网
2021年更新:我有一本书,它将把你从面向对象的泥潭中解放出来,简化函数式编程:一步一步的指南.
如果你想加入一个web开发者社区,在Elm中使用函数式编程来学习和帮助彼此开发web应用程序,请访问我的Facebook群,学习榆树编程https://www.facebook.com/groups/learnelm/
我的微博:@cscalfani