|
选自 towardsdatascience
[indent,今天来讲讲面向对象编程中比较棘手的问题。
[/indent,
20 世纪 60 年代,编程遇到了一个大问题:计算机还没有那么强大,需要以某种方式平衡数据结构和程序之间的能力。
这意味着,如果你有大量数据,那么不将计算机推向极限就无法充分利用这些数据。另外,如果你需要做很多事情,那么你就不能使用过多的数据,否则计算机将会一直运行下去。
接下来到了 1966、1967 年,Alan Kay 从理论上证明可以使用封装的微型计算机。这些微型计算机不共享数据,而是通过消息传递进行通信。这样就可以更加经济地使用计算资源。
尽管这个想法很巧妙,但直到 1981 年,面向对象编程才成为主流。在那之后,它就没有停止过吸引新的和经验丰富的软件开发者。面向对象的程序员市场一如既往地忙碌。
但是在最近几年中,这种已有几十年历史的编程范式受到越来越多的批评。难道是在面向对象编程大行其道 40 年之后,技术已经超越了这种范式?
上图文章链接:
函数和数据耦合
面向对象编程的主要思想非常简单:尝试将一个功能强大的程序整体分解为功能同样强大的多个部分。这样就可以将一些数据和那些只在相关数据上使用的函数耦合起来。
注意,这仅涵盖封装的概念。也就是说,位于对象内部的数据和函数对于外部是不可见的。我们只能通过消息(通常通过 getter 和 setter 函数)与对象的内容进行交互。
继承性和多态性并没有包含在最初的设计想法中,但是对于现在的面向对象编程而言是必需的。继承基本上意味着开发者可以定义具有其父类所有属性的子类。直到 1976 年,即面向对象的程序设计的概念问世十年之后,继承性才被引入。
又过了十年,多态性才进入面向对象的编程。简单来讲,这意味着某种方法或对象可以用做其他方法或对象的模板。从某种意义上说,多态性是继承性的泛化,因为并不是原始方法或对象的所有属性都需要传输到新实体。相反,你还可以选择重写一些属性。
多态性的特殊之处在于,即使两个实体在源代码中互相依赖,被调用实体的工作方式也更像插件。这使得开发人员的工作变得轻松,因为他们不必担心运行时的依赖关系。
值得一提的是,继承性和多态性并不是面向对象编程所特有的。真正的区别在于封装数据及其包含的方法。在计算资源比今天稀缺得多的时代,这是一个天才的想法。
面向对象编程中的 5 大问题
面向对象的编程一经问世,便改变了开发人员看待代码的方式。20 世纪 80 年代以前,过程式编程非常面向机器。开发人员需要非常了解计算机的工作原理才能编写好的代码。
通过封装数据和其他方法,面向对象的编程使软件开发更加以人为中心,符合人类的直觉。比如,方法 drive 属于 car 数据组,而不是 teddybear 组。之后出现的继承性也很直观。比如,现代汽车(Hyundai)是汽车的一个子类,并且具有相同的属性,但 PooTheBear 不是,这样很好理解。
香蕉猴子丛林问题
想象一下,你正在设置一个新程序,并且正在考虑设计一个新类。然后,你回想起为另一个项目创建的简洁的小类,发现其对正在进行的工作很合适。
没问题,你可以将以前项目中的类在新项目中复用。
这里有一个问题:这个类可能是另一个类的子类,因此你需要将它的父类也包含在内。然后你会发现,这个父类可能也是另一个类的子类,以此类推,最后要面对一堆代码。
Erlang 的创建者 Joe Armstrong 曾有一句名言:「面向对象语言的问题在于,它们自带其自身周围的所有隐式环境。你想要香蕉,但是得到的却是拿着香蕉的大猩猩和整个丛林。」
这几乎可以说明一切。复用类是可以的,实际上这可能是面向对象编程的主要优点,但不要将其发挥到极致。有时你应该建立一个新的类,而不是添加大量依赖项。
脆弱的基类问题
想象一下,如果你已经成功地将另一个项目中的类复用于新的代码,那么如果基类发生变化会怎样?
这可能会破坏你整个新项目的代码,即使你可能什么也没做。一旦有人更改了基类中的一个细节,而这一点又对你的项目至关重要,那么这种影响将是非常大并且突然的。
使用继承的次数越多,潜在的维护工作就越多。因此,即使在短期内复用代码非常有效,但从长远来看,它可能让你付出一定的代价。
菱形继承问题
利用继承可以将一类中的属性传递给其他类。但是,如果你想混合两个不同类的属性怎么办?
没错,这无法完成,至少常规的方法都不行。以 Copier 类为例(在此引用以下链接文章中的例子:http://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 个问题的共同点是它们都存在不合适的继承。由于继承没有包含在面向对象编程的原始形式中,所以这些问题可能不能称为面向对象本身的问题。
但是也并不是只有面向对象编程会被夸大。在纯粹的函数式编程中,处理用户的输入或在屏幕上输出消息极其困难。对此,面向对象或面向过程编程会好很多。
但仍然有一些开发人员试图将这些东西用纯函数的方式实现,并且编写几十行没人能看懂的代码。而使用另一种范式就能够轻松地将代码简化为几行可读的代码。
毫无疑问,函数式编程正在得到更多关注,而面向对象编程近几年遭到一些诟病。了解新的编程范式并在适当的时候使用它们是很有意义的。无论哪种编程范式,都不需要只遵循一种,在适当的时候使用不同的编程范式才能更好地解决问题。
上图文章链接:
面对越来越多的问题,函数式编程可能是更有效的一种选择。数据分析、机器学习、并行编程,这些领域你投入的越多,你就会越喜欢函数式编程。
但是目前面向对象开发的程序员的岗位需求量依然比函数式编程开发程序员多得多。但是这也并不意味着你不能成为后者,函数式编程开发的程序员目前仍然比较稀缺。
最有可能的情况是,面向对象的编程将会继续存在十年左右。当然,选择相对前卫的方式是好的,但这并不意味着你应该放弃面向对象编程。所以在接下来的几年中,不要完全放弃它,但至少确保它不是你唯一掌握的程序设计方式。
转载请联系本公众号获得授权 |
|