您的位置  > 互联网

作为程序员,你是使用函数式编程还是面向对象编程?

作为程序员,您是使用函数式编程还是面向对象编程?在这篇文章中,拥有10余年软件开发经验的作者,从面向对象编程的三大特点:继承、封装、多态性的角度提出了自己的问题,并深刻地表示,是时候告别面向对象编程了。

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

我曾经热衷于使用继承、封装和多态性。它们是范式的三大支柱。

我渴望在这个激动人心的新世界中实现再利用的美好,并享受我的前辈们积累的智慧。

一想到将现实世界中的所有内容映射到一个类中,以便可以整齐地规划整个世界,我就无法抑制自己的兴奋。

然而,我错得不能再错了。

继承,崩溃的第一支柱

乍一看,继承似乎是面向对象范式的最大优势。所有初学者教程都用最简单的继承示例来讨论继承,而这个似乎是合乎逻辑的。

然后是完全重用。甚至在那之后的所有东西都被重复使用。

我吞下了这一切,带着我的新发现冲向了这个世界。

香蕉猴丛林问题

怀着极大的信心和对解决问题的热情,我开始构建类的层次结构,然后编写代码。似乎一切都在控制之中。

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

然后我有了一个新项目,我想起了另一个我非常喜欢的项目。

没问题,重用可以节省一切。我只需要上那门课并使用它。

井。。。。。。事实上。。。。。。这不仅仅是那门课。你也必须带上父母。但。。。。。。我想没关系。

前额。。。。。。不,似乎还需要父类的父类......还有......好吧,我们需要所有的祖先阶级。好的 好的。。。。。。搞定了。没关系。

不错。但是它不编译,这是怎么回事?哦,我明白。。。。。。此对象还需要另一个对象。所以也必须带一个过来。没关系。。。。。。

等一会。。。。。。我不仅需要那个对象,还需要那个对象的父级,以及父级的父级,以及......所有祖传......包含的所有对象

唉。。。。。。

名言的创造者:

面向对象语言的问题在于它们依赖于特定的环境。你想要一根香蕉,但你得到了一只带香蕉的猩猩,你最终得到了整个丛林。

香蕉猴丛林的解决方案

这个问题的解决方案不是建立那么深的类层次结构。但是,如果继承是重用的关键,那么添加到继承机制的任何限制都将限制重用。右?

没错。

那么我们可怜的面向对象程序员呢?我们期望一杯三聚氰胺牛奶来维持我们的健康吗?

答案是:包含和委托(和)。稍后会详细介绍。

钻石继承问题

迟早,你会遇到以下令人作呕的问题,其中一些甚至根本无法解决。

大多数面向对象语言不支持这一点,尽管这似乎是合乎逻辑的。为什么面向对象语言很难支持这一点?

看看下面的伪代码:

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
 function start() {
 }
}
Class Printer inherits from PoweredDevice {
 function start() {
 }
}
Class Copier inherits from Scanner, Printer {
}

请注意,和 类都实现了一个名为 start 的方法。

那么问题来了,哪个开始继承?是还是是?绝对不可能同时继承它们。

菱形继承的解决

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

没错。大多数面向对象的人不会让你这样做。

但是,然而......如果你必须以这种方式建模怎么办?我需要重用!

然后,您必须使用包含和委派。

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
 function start() {
 }
}
Class Printer inherits from PoweredDevice {
 function start() {
 }
}
Class Copier {
 Scanner scanner
 Printer printer
 function start() {
 printer.start()
 }
}

注意:该类现在包含一个实例和一个实例。然后,将 start 函数委托给类的实现。委派也非常简单。

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

脆弱的基类问题

好的,所以我将尝试使用较浅的类层次结构,并确保其中没有戒指,这样就没有钻石继承。

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

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

嗯,也许这是一个错误......但是等等......确实有一些变化......

但改变的不是我的代码。看来变化来自我继承的班级。

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

那是。。。。。。

看看下面的基类(用 Java 编写,但即使你不懂 Java,它也应该很容易理解)。

import java.util.ArrayList;
public class Array
{
 private ArrayList a = new ArrayList();
 public void add(Object element)
 {
 a.add(element);
 }
 public void addAll(Object elements[])
 {
 for (int i = 0; i < elements.length; ++i)
 a.add(elements[i]); // this line is going to be changed
 }
}

重要提示:请注意带有注释的行。稍后对此行的更改将导致其他问题。

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

下面是继承的类:

public class ArrayCount extends 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;
 }
}

类是泛型 Array 类的专用化。两者之间行为的唯一区别是维护了一个计数,它记录了元素的数量。

让我们仔细看看这两个类别。

add() 的数组,将一个元素添加到本地元素。

数组的 () 调用每个元素的本地 add 方法。

add() 调用父类 add(),然后递增计数。

() 调用父类的 (),然后将许多元素添加到计数中。

一切都很正常。

现在就是问题所在。基类中带注释的代码行现在更改为如下所示:

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

从基类作者的角度来看,该类实现的功能根本没有改变。并且所有自动化测试都已通过。

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

现在 () 调用父级的 (),父级在内部调用 add(),并且 add() 被继承类重载。

因此,每次调用继承类的 add() 时,计数都会递增,然后在调用继承类的 () 时再次递增。

计数增加了两次。

由于发生这种情况,继承类的作者必须清楚基类是如何实现的。此外,对基类的每次更改都必须通知继承类的所有作者,因为这些更改可能会以不可预测的方式破坏继承的类。

唉!这个巨大的裂痕威胁着整个继承支柱的稳定性。

脆弱的基类变通办法

这个问题必须通过包容和授权来解决。

通过包含和委派,您可以从白盒编程转向黑盒编程。白盒编程意味着在编写继承类时,必须了解基类的实现。

另一方面,黑盒编程可以完全忽略基类的实现,因为不可能通过重载函数将代码注入到基类中。只需专注于界面即可。

这种趋势太烦人了......

继承应该导致最佳的重用。

在面向对象语言中实现包含和委派并不容易。它们旨在方便继承。

如果你和我一样,你会开始反思这份遗产。但更重要的是,这些问题应该促使您考虑通过层次结构进行分类。

层次结构问题

每次我到一家新公司时,我都会为将公司文件(即员工手册)存放在哪里而苦苦挣扎。

我应该创建一个文件夹并在其中创建一个文件夹吗?

或者我应该创建一个文件夹并在其中创建一个?

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

分层分类的思想是因为基类(父类)更通用,而继承类(子类)更专业化。继承链越往下,概念就越专业化(参见上面的形状层次结构)。

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

层次结构的解析

真正的问题是......

分层分类是错误的。

应该在哪里使用分层分类?

包含关系。

在现实世界中,有许多层次结构包含关系(或排他性)。

但是你找不到分层分类。想想吧。面向对象的范式是基于充满各种对象的现实世界构建的。但它使用了错误的模型——分层分类在现实世界中没有类似物。

但现实世界充满了等级关系。等级包含关系的一个很好的例子是你的袜子。袜子放在装袜子的抽屉里,然后抽屉放在衣柜里,衣柜放在卧室里,卧室放在房子里,依此类推。

硬盘上的目录是分层包含关系的另一个示例,它们包含文件。

那么我们如何对它们进行分类呢?

如果您考虑一下公司的文件,那么将它们放在哪里并不重要。我可以把它放在目录中,也可以把它放在 Stuff 目录中。

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

Document
Company
Handbook

标签没有顺序或分层(这也解决了钻石继承的问题)。

标签可以类似于界面,因为同一个文档可以有多种类型。

但既然裂缝如此之多,估计传承之柱已经坍塌了。

再见,继承。

封装,崩溃的第二大支柱

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

对象状态变量受到外部访问的保护,即它们被封装在对象内部。

我们不需要担心不知道谁访问的人可能会访问全局变量。

封装对于变量是安全的。

封装很棒!

封装万岁......

直到你遇到这个......

引用问题

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

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

如果将对某个对象的引用传递给另一个对象的构造函数,则构造函数可以将对该对象的引用放入私有变量中,并使用封装对其进行保护。

但是这个通行证的目标并不安全!

为什么不呢?这是因为其他代码也可能具有指向该对象的指针,例如调用构造函数的指针。它必须具有对对象的引用,否则无法传递给构造函数。

参考的分辨率

构造函数必须复制传递的对象。而且它不能是浅拷贝,它必须是深拷贝,即传入对象中包含的所有对象和所有对象中包含的所有对象......必须复制。

完全低效。

更糟糕的是,并非所有对象都可以复制。对于某些具有操作系统资源的对象,复制充其量是无效的,最坏的情况是根本无法复制。

所有主要的面向对象语言都存在这个问题。

再见,封装。

多态性,崩溃的第三大支柱

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

这就像三人组中的拉里·费恩(Larry Fine)。

无论他们走到哪里,他们都会带着他,但他永远是一个配角。

这并不是说多态性不好,而是因为它不需要面向对象的语言来实现多态性。

接口也可以是多态的,而没有面向对象的负担。

此外,该界面不限制您可以混合的不同行为的数量。

因此,毋庸置疑,我们可以告别面向对象的多态性,拥抱基于接口的多态性。

违背诺言

当然,面向对象在早期就承诺了很多。直到今天,这些承诺仍然在课堂、博客和在线资源中传授给年轻的程序员。

我花了好几年时间才意识到面向对象的谎言。我曾经年轻,容易上当受骗。

然后我发现我被骗了。

再见了,面向对象编程。

那该怎么办呢?

拥抱函数式编程。在过去的几年里,我一直很舒服地使用它。

但在最前线,我没有对你做出任何承诺。眼见为实。

有一次被蛇咬了十年,我害怕井绳。

你知道的。

原文:@/----

本站涵盖的内容、图片、视频等数据,部分未能与原作者取得联系。若涉及版权问题,请及时通知我们并提供相关证明材料,我们将及时予以删除!谢谢大家的理解与支持!

Copyright © 2023