20 世纪 60 年代,编程遇到了一个大问题:计算机还没有那么强大,需要以某种方式平衡数据结构和程序之间的能力。
这意味着,如果你有大量数据,那么不将计算机推向极限就无法充分利用这些数据。另外,如果你需要做很多事情,那么你就不能使用过多的数据,否则计算机将会一直运行下去。
接下来到了 1966、1967 年,Alan Kay 从理论上证明可以使用封装的微型计算机。这些微型计算机不共享数据,而是通过消息传递进行通信。这样就可以更加经济地使用计算资源。
尽管这个想法很巧妙,但直到 1981 年,面向对象编程才成为主流。在那之后,它就没有停止过吸引新的和经验丰富的软件开发者。面向对象的程序员市场一如既往地忙碌。
但是在最近几年中,这种已有几十年历史的编程范式受到越来越多的批评。难道是在面向对象编程大行其道 40 年之后,技术已经超越了这种范式?
函数和数据耦合
面向对象编程的主要思想非常简单:尝试将一个功能强大的程序整体分解为功能同样强大的多个部分。这样就可以将一些数据和那些只在相关数据上使用的函数耦合起来。
注意,这仅涵盖封装的概念。也就是说,位于对象内部的数据和函数对于外部是不可见的。我们只能通过消息(通常通过 getter 和 setter 函数)与对象的内容进行交互。
继承性和多态性并没有包含在最初的设计想法中,但是对于现在的面向对象编程而言是必需的。继承基本上意味着开发者可以定义具有其父类所有属性的子类。直到 1976 年,即面向对象的程序设计的概念问世十年之后,继承性才被引入。
又过了十年,多态性才进入面向对象的编程。简单来讲,这意味着某种方法或对象可以用做其他方法或对象的模板。从某种意义上说,多态性是继承性的泛化,因为并不是原始方法或对象的所有属性都需要传输到新实体。相反,你还可以选择重写一些属性。
多态性的特殊之处在于,即使两个实体在源代码中互相依赖,被调用实体的工作方式也更像插件。这使得开发人员的工作变得轻松,因为他们不必担心运行时的依赖关系。
值得一提的是,继承性和多态性并不是面向对象编程所特有的。真正的区别在于封装数据及其包含的方法。在计算资源比今天稀缺得多的时代,这是一个天才的想法。
面向对象编程中的 5 大问题
面向对象的编程一经问世,便改变了开发人员看待代码的方式。20 世纪 80 年代以前,过程式编程非常面向机器。开发人员需要非常了解计算机的工作原理才能编写好的代码。
通过封装数据和其他方法,面向对象的编程使软件开发更加以人为中心,符合人类的直觉。比如,方法 drive() 属于 car 数据组,而不是 teddybear 组。之后出现的继承性也很直观。比如,现代汽车(Hyundai)是汽车的一个子类,并且具有相同的属性,但 PooTheBear 不是,这样很好理解。
香蕉猴子丛林问题
想象一下,你正在设置一个新程序,并且正在考虑设计一个新类。然后,你回想起为另一个项目创建的简洁的小类,发现其对正在进行的工作很合适。
没问题,你可以将以前项目中的类在新项目中复用。
这里有一个问题:这个类可能是另一个类的子类,因此你需要将它的父类也包含在内。然后你会发现,这个父类可能也是另一个类的子类,以此类推,最后要面对一堆代码。
Erlang 的创建者 Joe Armstrong 曾有一句名言:「面向对象语言的问题在于,它们自带其自身周围的所有隐式环境。你想要香蕉,但是得到的却是拿着香蕉的大猩猩和整个丛林。」
这几乎可以说明一切。复用类是可以的,实际上这可能是面向对象编程的主要优点,但不要将其发挥到极致。有时你应该建立一个新的类,而不是添加大量依赖项。
脆弱的基类问题
想象一下,如果你已经成功地将另一个项目中的类复用于新的代码,那么如果基类发生变化会怎样?
这可能会破坏你整个新项目的代码,即使你可能什么也没做。一旦有人更改了基类中的一个细节,而这一点又对你的项目至关重要,那么这种影响将是非常大并且突然的。
使用继承的次数越多,潜在的维护工作就越多。因此,即使在短期内复用代码非常有效,但从长远来看,它可能让你付出一定的代价。
菱形继承问题
利用继承可以将一类中的属性传递给其他类。但是,如果你想混合两个不同类的属性怎么办?
没错,这无法完成,至少常规的方法都不行。以 Copier 类为例(在此引用以下链接文章中的例子:https://medium.com/@cscalfani/goodbye-object-oriented-programming-a59cda4c0e53),Copier 将扫描文件的内容并将其打印在白纸上。那么它应该是 Scanner 还是 Printer 的子类?
这个问题根本没有完美的答案。即使这个问题不会破坏你的代码,但它经常出现,会让人很沮丧。
层级问题
在菱形继承问题中,Copier 是哪个类的子类是问题的关键所在。但或许有个投机取巧的方案:假设 Copier 是父类,Scanner 和 Printer 是仅继承属性子集的子类,那么问题就解决了。
但如果你的 Copier 是黑白的,而 Printer 也能够处理彩色,那怎么办?从这个意义上说,Printer 不是 Copier 的一种泛化吗?如果 Printer 连接了 WiFi,而 Copier 没有呢?
类上堆积的属性越多,建立适当的层次结构就越困难。在你所处理的属性集群中,Copier 共享了 Printer 的一些属性,但不是全部属性,反之亦然。在大型复杂项目中,层次结构的问题会导致很大的混乱。
引用问题
你可能会想到进行没有层次结构的面向对象编程。我们可以使用属性集群,并根据需要继承、扩展或重写属性。也许这有点混乱,但这将是对当前问题的准确表示。
这里只存在一个问题:封装的全部目的是使数据片段彼此之间保持安全,从而使计算效率更高,但没有严格的层次结构,这是行不通的。
假设一个对象 A 通过与另一个对象 B 交互来覆盖层次结构,会发生什么情况?其他关系的情况并不重要,但当 B 不是 A 的直接父类时,A 必须包含 B 的全部私有引用,否则,它们将无法交互。
但是,如果 A 包含 B 的子类也具有的信息,那么就可以在多个位置修改该信息。因此,有关 B 的信息已经不再安全,并且封装已经被破坏。
尽管许多面向对象的程序员都使用这种架构来构建程序,但这并不是面向对象编程,只是一团糟。
单一范式存在的风险
以上 5 个问题的共同点是它们都存在不合适的继承。由于继承没有包含在面向对象编程的原始形式中,所以这些问题可能不能称为面向对象本身的问题。
但是也并不是只有面向对象编程会被夸大。在纯粹的函数式编程中,处理用户的输入或在屏幕上输出消息极其困难。对此,面向对象或面向过程编程会好很多。
但仍然有一些开发人员试图将这些东西用纯函数的方式实现,并且编写几十行没人能看懂的代码。而使用另一种范式就能够轻松地将代码简化为几行可读的代码。
毫无疑问,函数式编程正在得到更多关注,而面向对象编程近几年遭到一些诟病。了解新的编程范式并在适当的时候使用它们是很有意义的。无论哪种编程范式,都不需要只遵循一种,在适当的时候使用不同的编程范式才能更好地解决问题。
审核编辑 黄昊宇
全部0条评论
快来发表一下你的评论吧 !