本文转载自公众号“读芯术”(ID:AI_Discovery)。
编程在上世纪60年代遇到了一个大问题:计算机那时还没有那么强大,需要以某种方式在数据结构和进程之间分配容量。这意味着如果拥有大量数据,那么在不将计算机推向极限的情况下,很多事情将无法完成。另一方面,如果需要做很多事情,那么就不能使用过多的数据,否则计算机将永远占据空间。
接着艾伦·凯(AlanKay)大约于1966年或1967年得出理论认为可以使用封装的微型计算机,这些微型计算机不共享数据,而是通过消息传递进行通信。这样可以更加经济地使用计算资源。
尽管这个想法很巧妙,但直到1981年,面向对象编程才成为主流。但是从那以后,它并没有停止吸引软件开发新手和老手。面向对象编程的程序员一如既往的繁忙。
但近年来,这一已有十年历史的范式受到越来越多的批评。难道是在面向对象程序设计大行其道40年之后,技术已经超越了这种范式?
带数据的耦合函数是否可笑?
面向对象编程的主要思想非常简单:尝试将一个程序分解为功能强大的整体。随之而来的是,将数据片段和仅在相关数据上使用的那些函数耦合在一起。
请注意,这仅涵盖封装的概念。也就是说,位于对象内部的数据和函数对于外部是不可见的,一个人只能通过消息(通常称为getter和setter函数)与对象的内容进行交互。
继承和多态并没有包含在最初的想法中,但是对于当今的面向对象编程而言,这是必需的。继承基本上意味着开发人员可以定义具有其父类具有的所有属性的子类,直到1976年——面向对象的程序设计概念问世十年后,才将其引入。
十年后,多态进入了面向对象的程序设计。从根本上讲,这意味着方法或对象可以用作其他方法的模板。从某种意义上说,这是继承的概括,因为并非原始方法或对象的所有属性都需要传输给新实体;相反,可以选择覆盖属性。
多态的特殊之处在于,即使两个实体在源代码中相互依赖,被调用实体的工作方式也更像插件。这使开发人员的工作更加轻松,他们不必再担心运行时的依赖关系。
值得一提的是,继承和多态性并不是面向对象编程所独有的。真正的区别在于封装数据及其所属的方法。在那个计算资源比今天稀缺得多的时代,这是一个天才般的想法。面向对象的编程并不可笑,它使编码变得容易得多。
图源:unsplash
面向对象编程中的五大问题
面向对象编程一问世便改变了开发人员查看代码的方式。在1980年代以前,面向过程编程通常以机器为中心,开发人员需要非常了解计算机如何工作才能编写好的代码。
通过封装数据和方法,面向对象的编程使软件开发更加以人为中心。与人类的直觉相符,方法drive()属于数据组 car,但不属于teddybear组。当继承产生时,这也很直观。Hyundai是car的一个子类,并且具有相同的属性,但PooTheBear却不是,这是完全合理的。
这听起来像是一台强大的机器。但问题在于,只懂面向对象代码的程序员将会用这种思维方式思考他们所做的一切。就像人们到处看到钉子一样,因为他们只有锤子。正如我们将在下面看到的那样,当你的工具箱只有锤子时,可能会导致致命的问题。
1. 大猩猩丛林香蕉问题
如果你正在设置一个新程序,并且正在考虑设计一个新类。你可能会回想起为另一个项目创建的简洁的小类,并且意识到这对当前正在尝试的工作非常适合。没问题!可以将旧项目中的类重用于新项目。
除了该类实际上可能是另一个类的子类之外,因此现在还需要把父类包括在内。然后你意识到父类也依赖于其他类,并且最终包含了代码堆。
Erlang的创建者Joe Armstrong的这句话非常著名:“面向对象编程语言的问题在于,它们具有随身携带的所有隐式环境。你想要香蕉,但是得到的是一只拿着香蕉的大猩猩和整个丛林。”
这对此方法进行了很好的说明。可以重用类,实际上,这可能是面向对象编程的主要优点。但不要走极端,有时最好编写一个新类,而不是为了写重复代码而添加大量依赖项。要灵活变通,不要死板地遵从某个范式。
图源:unsplash
2. 脆弱的基类问题
如果已经成功地将另一个项目中的类重用于新代码,那么基类会发生怎样的变化?
它可能会破坏整个代码,而你甚至可能都没有碰过它。也许有一天你手上的项目熠熠生辉,而第二天却被打回原形,因为有人更改了基类中的一个细微细节,而该细节最终对项目至关重要。
使用继承的次数越多,潜在的维护工作就越多。因此,即使在短期内重用代码似乎非常有效,但从长远来看,它可能会带来很大的代价。
3. 钻石问题
继承是一件可爱的小事,可以在其中继承一类的属性并将其转移给其他类。但该如何组合两个不同类的属性?
这也许做不到,至少没办法以简洁的方式做到,例如Copier类。(笔者从Charles Scalfani的热门文章《再见,面向对象的编程》中借用了这个示例以及有关此处出现的问题的一些信息。)复印机扫描文档的内容并将其打印在空白纸上,它应该是Scanner还是Printer的子类?
根本没有好的答案。即使这个问题不会破坏代码,但它经常出现足以令人沮丧。
4. 层次问题
在钻石问题中,问的是Copier是哪个类的子类。但其实我话没说完,有一个简单的解决方案。假设Copier 是父类,而Scanner 和Printer是仅继承属性子集的子类。
这就变得很简单。但如果Copier只是黑白复印,而Printer还可以彩色打印怎么办?从这个意义上说,打印机不是包括复印机的吗?如果打印机连接到WiFi但复印机没有连接怎么办?
在类上堆积的属性越多,建立适当的层次结构就越困难。确实,在处理属性集群时,其中Copier共享了Printer的部分但不是全部属性,反之亦然。而且,如果尝试将其置于层次结构中,并且是一个大型复杂项目,则可能会导致混乱。不要混淆层次结构,否则可能会陷入混乱。
图源:unsplash
5. 参考问题
有人也许会说那么我们将进行没有层次结构的面向对象编程。其实相反,我们可以使用属性集群,并根据需要继承、扩展或覆盖属性。这会有些混乱,但这将是对当前问题的准确表现。
还有一个问题。封装的全部目的是使数据片段彼此之间保持安全,从而使计算效率更高。没有严格的层次结构,这是行不通的。
如果一个对象A通过与另一个对象B交互来覆盖层次结构,会发生什么?A与B的关系并不重要,除了B不是直接的父类。然后,A必须包含对B的私有引用,否则,将无法交互。但是,如果A包含B的子代也具有的信息,则可以在多个位置修改该信息。因此,有关B的信息已不再安全,并且封装被破坏。
尽管许多面向对象的程序员都使用这种架构来构建程序,但这并不是面向对象的编程,只是一团糟。
单一范式的危险
这五个问题的共同点是它们在不是最佳解决方案的地方实现了继承。由于继承甚至没有包含在面向对象编程的原始形式中,因此笔者不会将这些问题称为面向对象固有的问题,它们只是太过教条式的例子。
但是,不仅面向对象的编程可能会被夸大。在纯函数式编程中,处理用户输入或在屏幕上打印消息极为困难。出于这些目的,面向对象或过程编程要好得多。
仍然有一些开发人员尝试将这些东西实现为纯函数,并将其代码分解为数十行,没人能理解。使用另一种范式,他们可以轻松地将代码简化为几行可读的代码。
范式有点像宗教,它们都具有一定的合理性,耶稣、穆罕默德和佛陀说了一些很酷的话。但是,如果一直遵循教条,可能最终会使自己和周围人的生活痛苦不堪。编程范式也是如此。毫无疑问,函数式编程正逐渐受到人们的欢迎,而在过去的几年中,面向对象的编程遭到了一些严厉的批评。
了解新的编程范式并在适当的时候使用它们是有意义的。如果面向对象编程是使开发人员无论走到哪里都能看到钉子的锤子,那是把锤子扔出窗户的原因吗?不是。你在工具箱中添加了一把螺丝刀,也许是一把刀或一把剪刀,你不过是根据当前问题选择工具。
函数式编程和面向对象编程的程序员都不要像对待宗教那样对待编程范式。它们是工具,都可以在某处使用,所使用的内容仅取决于待解决的问题。
一个大问题:我们正处于一场新革命的风口浪尖上吗?
图源:unsplash
归根结底,关于函数式编程和面向对象编程的争论(相当激烈)可以归结为这一点:是否可以迈入面向对象编程时代的尽头?
函数式编程通常是更有效的选择,越来越多的问题出现。如数据分析、机器学习和并行编程,对这些领域的投入越多,就会越喜欢函数式编程。但看看现状,有十多种面向对象编程的程序员提供的产品,还有一种针对函数式编码器的产品。这并不意味着你不会喜欢这份工作,如今,函数式编程开发人员仍然非常稀缺。
最有可能的情况是,面向对象的编程将继续存在十年左右。函数式编程当然会越来越受欢迎,但这并不意味着应该放弃面向对象编程。把面向对象编程作为保留技能仍然非常有优势。
因此,在接下来的几年中,不要将面向对象的编程丢到工具箱外,但是请确保它不是你唯一的工具。