不久前,中心上出现了几篇文章,它们将功能和对象方法进行了对比,这些评论在评论中引发了对其真正意义的热烈讨论-面向对象的编程以及它与功能的区别。 我虽然迟了一点,但仍想与他人分享罗伯特·马丁(Robert Martin)(也称为鲍勃叔叔)对此的看法。
在过去的几年中,我反复能够与那些对OOP有偏见的学习函数式编程的人们一起进行编程。 这通常以以下语句的形式表示:“嗯,这太像某个对象。”
我认为这是因为FP和OOP是互斥的。 许多人似乎认为,如果程序具有功能,那么它就不是面向对象的。 我认为,形成这种观点是对新事物进行研究的必然结果。
当我们采用一种新技术时,我们通常会开始避免使用以前使用的旧技术。 这是自然的,因为我们认为新技术“更好”,因此旧技术可能“更糟”。
在这篇文章中,我有理由认为,尽管OOP和FP是正交的,但它们并不是互斥的概念。 一个好的功能程序可以(并且应该)是面向对象的。 而且一个好的面向对象程序可以(并且应该)起作用。 但是为了做到这一点,我们必须确定条款。
什么是面向对象?
我将从还原主义的角度处理这个问题。 OOP有许多正确的定义,涵盖了许多概念,原理,技术,模式和哲学。 我打算忽略它们,而是专注于盐本身。 在这里,需要简化主义,因为围绕OOP的所有这些机会并不是真正面向OOP的; 通常,这只是在软件开发中发现的大量机会的一部分。 在这里,我将重点介绍OOP的一部分,它是定义的且不可删除的。
看两个表达式:
1:f(o); 2:的();
有什么区别?
显然没有语义上的区别。 整个差异完全在语法上。 但是一个看起来是过程性的,另一个是面向对象的。 这是因为我们已经习惯了这样一个事实,即表达式2隐含了表达式1没有的特殊行为语义,这种特殊的行为语义是多态性。
当我们看到表达式1时,我们看到函数f ,对象o被转移到函数f中 。 这意味着只有一个名为f的函数,而不是它是围绕o的标准函数组的成员这一事实。
另一方面,当我们看到表达式2时,我们看到一个名称为o的对象,并将名称为 f的消息发送到该对象。 我们期望可能还有其他类型的对象接收消息f,因此我们不知道调用后从f期望什么特定的行为。 行为取决于类型o。 也就是说,f是多态的。
我们从多态行为方法中期望的这一事实是面向对象编程的本质。 这是一个简化的定义,不能从OOP中删除此属性。 没有多态性的OOP不是OOP。 所有其他OOP属性,例如数据封装以及与此数据相关的方法,甚至继承,都与表达式1相关,而不是与表达式2相关。
使用C和Pascal(在某种程度上甚至包括Fortran和Cobol)的程序员始终创建了封装功能和结构的系统。 要创建这样的结构,您甚至不需要面向对象的编程语言。 这种语言中的封装甚至简单的继承都是显而易见且自然的。 (在C和Pascal中,比其他人更自然)
因此,真正区别OOP程序和非OOP程序的是多态性。
您可能要争辩说,仅通过使用内部f switch或使用长if / else链就可以完成隐喻。 的确如此,因此我需要为OOP设置另一个限制。
使用多态不应导致调用方对被调用方的依赖。
为了解释这一点,让我们再次看一下这些表达式。 表达式1:f(o)似乎取决于源代码级别的函数f。 我们得出这一结论,是因为我们还假设f仅为一个,因此呼叫者必须知道被呼叫者。
但是,当我们查看()的表达式2时,我们会假设其他情况。 我们知道f可以有很多实现,并且我们不知道实际上将调用这些函数中的哪个。 因此,包含表达式2的源代码与在源代码级别调用的函数无关。
更具体地说,这意味着包含多态函数调用的模块(带有源代码的文件)不应引用包含这些功能的实现的模块(带有源代码的文件)。 不能包含 , 使用或要求任何其他使某些源代码文件依赖于其他源代码文件的关键字。
因此,我们对OOP的简化主义定义是:
一种使用动态多态性来调用函数,并且不会在源代码级别上创建调用者对被调用者的依赖项的技术。
什么是AF?
再一次,我将使用还原论方法。 FP具有丰富的传统和历史,其渊源比编程本身更深。 有一些原理,技术,定理,哲学和概念渗透到这个范式中。 我将忽略所有这些内容,直接进入本质,将FP与其他样式区分开来的固有属性。 这是:
如果a == b,则f(a)== f(b)。
在功能程序中,无论程序运行了多长时间,使用相同参数调用函数都会得到相同的结果。 有时称为参照透明性。
由上可知,f不应更改影响f行为的全局状态部分。 此外,如果我们说f代表系统中的所有功能-也就是说,系统中的所有功能必须是参照透明的-那么系统中的任何功能都无法更改全局状态。 没有任何函数可以做的事情会导致系统返回具有相同参数的不同值的另一个函数。
这会产生更深的后果-无法更改命名值。 也就是说,没有赋值运算符。
如果仔细考虑这一陈述,可以得出结论,仅由透明透明的函数组成的程序无法执行任何操作-因为系统的任何有用行为都会改变某些事物的状态。 即使只是打印机或显示器的状态。 但是,如果我们将铁排除在参照透明性和周围世界所有元素的要求之外,那么事实证明我们可以创建非常有用的系统。
当然,重点是递归。 考虑一个采用状态为参数的结构的函数。 此参数包含函数需要运行的所有状态信息。 工作完成后,该函数将创建一个新的结构,其状态与上一个状态不同。 在执行最后一个操作时,该函数以新结构作为参数调用自身。
这只是功能程序可以用来存储状态更改而不必更改状态的简单技巧之一[1]。
因此,函数式编程的简化派定义:
参照透明度-您无法重新分配值。
FP与OOP
此时,OOP的支持者和FI的支持者都已经通过光学瞄准器看着我。 还原主义不是结交朋友的最佳方法。 但有时它很有用。 在这种情况下,我认为阐明不褪色的反OOP大头照很有用。
显然,我选择的两个简化派定义是完全正交的。 多态性和参照透明性彼此无关。 它们不以任何方式相交。
但是正交性并不意味着相互排斥(请问James Clerk Maxwell)。 创建使用动态多态性和参照透明性的系统是完全可能的。 这不仅是可能的,而且是正确的!
为什么这个组合很好? 出于两个组件完全相同的原因! 建立在动态多态性之上的系统是好的,因为它们的连接性很低。 依赖关系可以颠倒过来,放在架构边界的不同侧。 可以使用Moki和Fake以及其他类型的Test Doubles对这些系统进行测试。 可以修改模块,而无需更改其他模块。 因此,这样的系统更易于修改和改进。
建立在参照透明性上的系统也是好的,因为它们是可预测的。 状态不变性使此类系统更易于理解,更改和改进。 这大大降低了出现争用和其他多线程问题的可能性。
这里的主要思想是:
没有holivar FP vs OOP
FP和OOP可以很好地合作。 两者都很好,适合在现代系统中使用。 该系统基于OOP和FP原理的组合,可最大程度地提高灵活性,可维护性,可测试性,简便性和强度。 如果删除一个添加另一个,只会使系统的结构恶化。
[1]由于我们使用的是冯·诺依曼(Von Neumann)架构的机器,因此我们假设它们具有状态实际上发生变化的存储单元。 在我描述的递归机制中,尾部尾部递归优化将不允许创建新的玻璃框架,而将使用原始玻璃框架。 但是这种对引用透明性的侵犯(通常)对程序员来说是隐藏的,不会影响任何事情。