再见,面向对象编程

2020-06-27 00:22:15

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

我热衷于利用继承、封装和多态的好处。范式的三大支柱。

我渴望得到重用的承诺,并利用在我之前的人在这个新的和令人兴奋的场景中获得的智慧。

一想到要将我的真实世界对象映射到它们的类中,我就抑制不住自己的兴奋,并期望整个世界都能整齐地落在适当的位置。

乍一看,继承似乎是面向对象范例的最大好处。作为新灌输的例子,所有简单的形状层次结构的例子似乎都是合乎逻辑的。

再利用是当今的流行语。没有…。让它成为这一年,也许永远都是。

怀着信仰和要解决的问题,我开始构建类层次结构并编写代码。这个世界一切都很好。

我永远不会忘记那一天,当时我准备通过继承现有类来实现重用的承诺。这是我一直在等待的时刻。

一个新的项目出现了,我回想起我在上一个项目中非常喜欢的那节课。

没问题。再用一次来营救。我要做的就是简单地从另一个项目中获取那个类并使用它。

井…。实际上是…。不只是那个班级。我们需要家长班。但是…。但仅此而已。

…。等待…。看来我们还需要父母的父母。然后是…。我们需要所有的家长。好的,…。好的,…。我来处理这件事。没问题。

而且很棒。现在它不能编译了。为什么??。哦,我看到…了。此对象包含此其他对象。所以我也需要那个。没问题。

等待…。我不只需要那个东西。我需要对象的父级和其父级的父级,依此类推,每个包含的对象及其包含的所有父级,以及它们的父级、父级、父级的…。

面向对象语言的问题是它们有所有这些隐含的环境,它们随身携带。你想要一根香蕉,但你得到的是一只大猩猩拿着香蕉和整个丛林。

我可以通过不创建太深的层次结构来驯服这个问题。但是,如果继承是重用的关键,那么我对该机制施加的任何限制肯定会限制重用的好处。对吗?

那么,一个可怜的面向对象程序员,一个得到了Kool-aid的健康帮助的人,能做些什么呢?

迟早,下面的问题会露出难看的样子,而且根据语言的不同,会出现无法解决的问题。

大多数面向对象语言都不支持这一点,尽管这似乎合乎逻辑。在面向对象语言中支持它有什么困难呢?

类PoweredDevice{}类扫描仪继承自PoweredDevice{function start(){}}类打印机继承自PoweredDevice{function start(){}}类复印机继承自扫描仪,打印机{}。

请注意,Scanner类和Printer类都实现了名为Start的函数。

那么,Copier类继承了哪个Start函数呢?扫描仪那台?打印机那台吗?不可能两者兼而有之。

是的没错。大多数面向对象语言都不允许您这样做。

类PoweredDevice{}类扫描仪继承自PoweredDevice{function start(){}}类打印机继承自PoweredDevice{function start(){}}类复印机{扫描仪扫描仪打印机打印机函数start(){printer.start()}}

请注意,这里的Copier类现在包含Printer和Scanner的实例。它将Start函数委托给Printer类的实现。它可以很容易地委托给扫描仪。

所以我让我的等级制度变得肤浅,让它们不再是周期性的。我没有钻石。

前一天,我的代码起作用了,第二天它就停止工作了。这是最重要的一点。我没有更改我的密码。

嗯,也许这是个BUG…。但是等等…。确实有什么东西改变了…。

但这不在我的代码里。原来这个变化发生在我继承的那个班级里。

想象一下下面的基类(它是用Java编写的,但是如果您不了解Java,它应该很容易理解):

导入java.util.ArrayList;public class Array{private ArrayList<;object>;a=new ArrayList<;object>;();public void add(Object Element){a.add(Element);}public void addAll(object element[]){for(int i=0;i<;elements.length;++i)a.add(element[i]);//此行将要更改}}。

重要提示:请注意注释的代码行。这条线稍后要改了,会把东西弄坏的。

该类的接口上有两个函数,add()和addAll()。add()函数将添加单个元素,addAll()将通过调用add函数添加多个元素。

公共类ArrayCount扩展Array{private int count=0;@Override public void add(Object Element){super.add(Element);++count;}@Override public void addAll(Object Elements[]){super.addAll(Elements);count+=elements.length;}}。

ArrayCount类是常规Array类的专门化。唯一的行为区别是ArrayCount保留元素数量的计数。

Array add()将一个元素添加到本地ArrayList。Array addAll()为每个元素调用本地ArrayList add。

ArrayCount add()调用其父函数的add(),然后递增计数。ArrayCount addAll()调用其父函数的addAll(),然后按元素数递增计数。

现在轮到突破性的改变了。Base类中的注释代码行更改为以下内容:

public void addAll(Object Elements[]){for(int i=0;i<;elements.length;++i)add(Elements[i]);//此行已更改}。

就基类的所有者而言,它仍然像广告中所宣传的那样起作用。而且所有的自动化测试仍然通过。

但是所有者没有注意到派生类。派生类的所有者将被粗暴地唤醒。

现在ArrayCount addAll()调用其父类的addAll(),后者在内部调用已被派生类覆盖的add()。

这会导致每次调用派生类的add()时计数都会递增,然后再按派生类的addAll()中添加的元素数递增。

如果这种情况可以发生,并且确实发生了,则派生类的作者必须知道基类是如何实现的。而且必须通知他们基类中的每个更改,因为这可能会以不可预测的方式破坏他们的派生类。

通过使用Container和Delegate,我们从白盒编程到黑盒编程。使用白盒编程,我们必须查看基类的实现。

使用黑盒编程,我们可以完全不了解实现,因为我们不能通过覆盖Base类的一个函数将代码注入到Base类中。我们只需关注界面。

面向对象的语言不会让包含和委托变得容易。它们的设计目的是让继承变得容易。

如果你和我一样,你会开始怀疑继承的问题。但更重要的是,这应该动摇您对通过层次结构进行分类的能力的信心。

每次我开始在一家新公司工作时,当我创建一个地方来放置我的公司文档时,我都会努力解决这个问题,比如“员工手册”(Employee Handbook)。

我要先创建一个名为Documents的文件夹,然后在其中创建一个名为Company的文件夹吗?

或者我创建一个名为Company的文件夹,然后在其中创建一个名为Documents的文件夹?

分类层次结构的思想是有更通用的基类(父类),派生类(子类)是这些类的更专门化版本。当我们沿着继承链往下走时,甚至会变得更加专业化。(请参见上面的形状层次结构)。

但是,如果父母和孩子可以随意互换位置,那么这种模式显然是有问题的。

如果你看一下现实世界,你会发现到处都是遏制(或独占所有权)的层次结构。

您找不到的是绝对层次结构。让我们暂时领会这一点。面向对象的范式是以充满对象的真实世界为前提的。但是它使用了一个破碎的模型,也就是说。范畴层次结构,其中没有现实世界的类比。

但现实世界充满了遏制等级制度。一个很好的例子就是你的袜子。它们在一个袜子抽屉里,这个抽屉放在你梳妆台的一个抽屉里,这个抽屉放在你的卧室里,而卧室里放在你的房子里,等等。

硬盘上的目录是包含层次结构的另一个示例。它们包含文件。

嗯,如果你想一想公司的文件,我把它们放在哪里几乎没有关系。我可以把它们放在一个文档文件夹里,或者放在一个叫做“材料”的文件夹里。

我的分类方式是用标签。我用以下标记标记该文件:

标记类似于接口,因为您可以有多种类型与文档相关联。

对象状态变量受到保护,不会被外部访问,也就是说,它们被封装在对象中。

我们再也不用担心被谁知道的人访问的全局变量了。

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

这意味着函数不会传递对象,而是传递指向对象的引用或指针。

如果对象是通过引用传递给对象构造函数的,则构造函数可以将该对象引用放入受封装保护的私有变量中。

为什么不行?因为另一段代码有一个指向该对象的指针,即。调用构造函数的代码。它必须具有对对象的引用,否则无法将其传递给构造函数?

构造函数必须克隆传入的对象。而不是浅克隆而是深克隆,即包含在传入对象中的每个对象以及这些对象中的每个对象等等。

这是最重要的一点。并非所有对象都可以克隆。有些拥有与之相关的操作系统资源,这使得克隆在最好的情况下毫无用处,或者在最坏的情况下不可能。

这并不是说多态不好,只是你不需要面向对象的语言就能做到这一点。

有了Interfaces,您可以混合的不同行为的数量没有限制。

因此,不费吹灰之力,我们就告别了OO多态,并问候了基于接口的多态。

嗯,OO在早期肯定承诺了很多。这些承诺仍然是对坐在教室里、阅读博客和参加在线课程的天真程序员做出的。

我花了很多年才意识到OO是怎么骗我的。我也是睁大眼睛,缺乏经验,轻信别人。

你好,函数式编程。在过去的几年里和你一起工作真是太好了。

如你所知,我不会轻信你的任何承诺。我得亲眼看看才能相信。

如果您喜欢此内容,请单击下面的💚,以便其他人在Medium上看到此内容。

如果您想加入一个使用ELM函数式编程互相学习和帮助开发Web应用程序的Web开发人员社区,请查看我的facebook群,学习ELM编程https://www.facebook.com/groups/learnelm/