本文最初发表于 Towards Data Science 博客,经原作者 Rhea Moutafis 授权,InfoQ 中文站翻译并分享。
在 20 世纪 60 年代,编程有一个很大的问题:计算机还远没有那么强大,而且不知何故,它们还需要在数据结构和过程之间以某种方式分配容量。
这意味着,如果你有大量数据的话,你就不能用它做那么多的事情,否则就会把计算机逼到极限。而另一方面,如果你需要做很多事情,你就不能使用太多的数据,否则计算机将会耗费大量时间。
后来,Alan Lay 在 1966 年或 1967 年提出了一个理论,即人们可以使用封装起来的微型计算机,这种计算机并不共享数据,而是通过消息传递并进行通信。通过这种方式,可以做到更经济地使用计算资源。
尽管这个想法很有创意,但直到 1981 年,面向对象编程才成为主流。然而,从那时起,它就一直吸引着新入门的和经验丰富的软件开发人员。面向对象的程序员市场一如既往地繁忙。
但近年来,这一有着十年历史的范式受到了越来越多的批评。有没有可能,在面向对象编程普及四十年后,技术已经超越了这种方式?
耦合函数与数据有那么愚蠢吗?
面向对象编程背后的主要思想非常简单:试图将一个程序分解成与整体一样强大的部分。接下来,你需要将数据片段和那些仅在相关数据上使用的函数耦合在一起。
请注意,这仅仅只是涵盖了封装的概念,也就是说,位于对象内部的数据和函数对外部都是不可见的。人们只能通过消息与对象的内容进行交互,通常称为getter
和setter
函数。
最初的想法中没有包含的,但被认为是当今面向对象编程必不可少的是:继承和多态性。继承基本上意味着开发人员可以定义子类,这些子类拥有其父类所拥有的所有属性。直到 1976 年才被引入到面向对象编程中,也就是在其概念提出十年之后。
又十年后,多态性才进入面向对象编程。由基本概念而言,多态性意味着一个方法或一个对象可以作为其他方法或对象的模板。从某种意义上说,这是继承的泛化,因为并非原始方法或对象的所有属性都需要传输到新实体;相反,你可以选择重载属性。
多态性的特殊之处在于,即使源代码中两个实体相互依赖,被调用的实体的工作方式也更像插件。这使得开发人员的工作更加轻松,因为他们不必担心运行时的依赖关系。
值得一提的是,继承和多态性并不是面向对象编程所独有的。真正的不同之处在于封装数据片段和属于它们的方法。在计算资源比今天稀缺的时代,这真的是一个天才的想法。
面向对象编程中的五大问题
面向对象编程一经问世,它就改变了开发人员看待代码的方式。在 20 世纪 80 年代以前盛行的过程式编程,是非常面向机器的。开发人员需要对计算机如何工作有相当多的了解,才能写出好代码。
通过封装数据和方法,面向对象编程使软件开发更加以人为本。它符合人类的直觉,方法drive()
属于数据组car
,但不属于组teddybear
。
继承出现的时候,那也是直观的。Hyundai
汽车是car
的一个子群,有着相同的属性,这是完全合理的,但PooTheBear
却并非如此。
这听起来像是一台强大的机器。然而,问题是,只知道面向对象编程的程序员会将这种思维方式强加于他们所做的每件事上。就像人们看到处都是钉子一样,因为他们只有一把锤子。正如我们将在下面看到的,当你的工具箱中只有一把锤子的时候,这可能会导致致命的问题。
香蕉大猩猩丛林问题
假设你正在设置一个新程序,并且你正在考虑设计一个新的类。然后,你回想起为另一个项目创建的一个简洁的小类,你意识到它对你目前正在尝试做的事情来说是非常完美的。
没问题!你可以在新项目中重用旧项目中的类。
除了这个类实际上可能是另一个类的子类外,所以现在还需要包含父类。然后,你意识到父类也依赖于其他类,如此一来,最终会包含大量的代码。
Erlang 的创造者 Joe Armstrong 曾说过一句名言:
面向对象编程的问题是,默认带有环境。你只想要一个香蕉,但是得到了一只拿着香蕉的大猩猩,甚至还有整个丛林。
这几乎说明了一切。重用类是很好的做法。事实上,它可以成为面向对象编程的一个主要优点。
但不要走极端。有时候,为了避免 DRY(don’t repeat yourself 不要重复自己),你最好编写一个新的类,而不是包含大量的依赖项。
脆弱的基类问题
假设你已经成功地为新代码重用了来自另一个项目的一个类。如果基类发生变化,会发生什么情况?
它有可能会破坏你的整个代码。甚至你可能连碰都没有碰过。但是,有一天你的项目工作得很顺利,但第二天就不这样了,因为有人更改了基类中的一个小细节,而这个细节最终对你的项目非常重要。
使用继承的次数越多,可能需要进行的维护就越多。因此,尽管重用代码在短期内看起来非常有效,但从长远来看,它可能会付出高昂的代价。
钻石问题
继承是可爱的小东西,我们可以把一个类的属性转移给其他类。但如果你想混合两个不同类的属性呢?
嗯,你做不到。至少不会是以一种优雅的方式。例如,以类Copier
为例。(我从 Charles Scalfani 的文章《再见,面向对象编程》(Goodbye, Object Oriented Programming)中借用了这个例子,以及一些关于这里提出的问题的信息)复印机扫描文档内容并将其打印在控制上。那么它应该是Scanner
的子类,还是Printer
的子类呢?
根本没有好的答案。即使这个问题不会破坏你的代码,它也会经常出现,令人沮丧。
层次问题
在钻石问题中,问题是Copier
是哪个类的子类。但我骗了你——因为有一个很好的解决方案。假设Copier
是父类,Scanner
和Printer
是仅继承属性子集的子类。问题解决!
这很好。但是,如果你的Copier
只是黑白复印机,而你的Printer
能处理彩色,那怎么办呢?从这个意义上说,Printer
不就是Copier
的泛化吗?如果Printer
连接到 WiFi,但Copier
没有连接,又该怎么办?
在类上堆积的属性越多,建立适当的层次结构就越困难。实际上,你要处理的是一组属性,其中Copier
共享Printer
的一些属性,但不是所有属性,反之亦然。如果你试图将它固定到层次结构中,而你有一个庞大而复杂的项目,这可能会导致异常混乱的灾难。
引用问题
你可能会说,好吧,那我们就只做面向对象的编程,不带层次结构。相反,我们可以使用属性集群,并根据需要继承、扩展或覆盖属性。当然,这可能会有点混乱,但它将是手头问题的准确描述。
只有一个问题。封装的全部意义在于保证数据片段之间的安全,从而使计算更加高效。如果没有严格的层次结构,这是行不通的。
考虑如果一个对象A
通过另一个对象B
交互来覆盖层次结构会发什么。A
与B
有什么关系并不重要,重要的是B
不是直接的父类。那么A
就必须包含对B
的私有引用。否则,它将无法交互。
但是,如果A
包含B
的子类也拥有的信息,那么这些信息就可以在多个地方被修改。因此,关于B
的信息已不再安全,并且封装也被破坏了。
尽管许多面向对象的程序员使用这种架构来构建程序,但这不是面向对象编程。只是一团糟。
单一范式的危险
这五个问题的共同之处在于,它们在不是最佳解决方案的地方实现了继承。由于继承甚至没有包含在面向对象编程的原始形式中,我不会将这些问题称为面向对象的固有问题。它们只是一个教条走得太远的例子。
但是,不仅仅是面向对象编程可能做过头了。在纯函数式编程中,在屏幕上处理用户输入或打印消息是极其困难的。对于这些目的,面向对象或过程化编程要更好一些。
尽管如此,仍有一些开发人员试图将这些东西作为纯函数来实现,并将他们的代码扩展到几十行,以至于没有人能够理解。使用另一种范式,他们可以轻松地将代码简化为几行可读的代码。
范式有点像宗教。它们在适度的情况下是好的:可以说,耶稣、穆罕默德和佛陀都说过一些很酷的话。但是如果你跟随他们到最后一个小细节,你可能最终会让自己和周围人们的生活变得相当悲惨。
编程范式也是如此。毫无疑问,函数式编程越来越受欢迎,而面向对象编程在过去几年来受到了一些严厉的批评。
了解新的编程范式并在适当的时候使用它们是有意义的。如果说,面向对象编程是让开发人员无论走到哪里都能看到钉子和锤子,那这是不是将锤子扔出窗外的理由呢?不是的。你可以在工具箱中添加一把螺丝刀,或者一把小刀、一把剪刀,然后根据手头的问题来选择工具。
函数式和面向对象的程序员一样,不要将你的范式当做宗教来对待。它们只是工具,都有自己的用途。你使用什么工具应该只取决于你正在解决什么问题。
最大问题是:我们是否正处于一场新革命的风口浪尖?
归根结底,关于函数式编程与面向对象编程的争论(不可否认,争论是相当激烈的)归结为这样一个问题:面向对象编程的时代会不会走到尽头?
在函数式编程通常是更有效的选择的情况下,出现了越来越多的问题。请想一想数据分析、机器学习和并行编程。你越是深入这些领域,就会越喜欢函数式编程。
但是如果你看一下现状,面向对象的程序员有很多工作机会,而函数式程序员只有寥寥可数的工作机会。这并不意味着如果你喜欢后者就找不到工作;现在函数式开发人员仍然相当稀缺。
最有可能的情况是,面向对象编程将再持续十年左右。当然,先锋派是函数式编程,但这并不意味着你应该抛弃面向对象编程。面向对象编程仍然是令人难以置信的好东西。
因此,在接下来的几年,不要将面向对象编程从你的工具箱扔出去。但要确保它不是你唯一的工具。
作者介绍:
Rhea Moutafis,正在攻读暗物质物理学博士学位。热爱艺术、音乐及美好事物。
原文链接:
https://towardsdatascience.com/object-oriented-programming-is-dead-wait-really-db1f1f05cc44
评论 18 条评论