[译] FP vs OO

原文地址:https://blog.cleancoder.com/uncle-bob/2018/04/13/FPvsOO.html

原作者:Robert C. Martin (Uncle Bob)

在过去的几年中,我通过与人结对来学习函数式编程,他们中的很多人表达了反对OO的偏见。他们经常会说:“啊,这太像对象了。”

他们会这样说是因为他们认为FP和OO在某种程度上是互斥的。许多人似乎认为程序FP的程度等同于其非OO的程度。我认为这种观点是学习新事物的自然结果。

当我们采用一种新技术时,我们通常倾向于避开以前使用的旧技术。这很自然,因为我们认为新技术“更好”,因此旧技术就一定是“更糟”的。

在此博客中,我将说明OO和FP是正交的,但它们并不互斥。一个好的函数式程序可以(并且应该)是面向对象的。而且一个好的面向对象程序可以(并且应该)是函数式的。在此之前,我们必须非常谨慎地给FP和OO这两个词语下个定义。

什么是OO?

我将在这里采取非常还原主义的立场。OO有许多有效的定义,涵盖了丰富的概念,原理,技术,模式和哲学。在这里,我将忽略所有这些内容,而将重点放在最基础的东西上。我采取这种还原主义的原因是,很多围绕着OO的丰富特性实际上根本不是OO所特有的,而是整体软件开发丰富性的一部分。在这里,我将重点介绍面向对象不可分割的那部分。

看看以下两个表达式:

1
2
1f(o);
2:o.f();

有什么区别?

显然,没有实际的语义差异。差异完全在语法上。但是一个看起来是FP的,另一个看起来是OO的。这是因为我们会推断表达式2具有特殊的语义行为,同时我们推断表达式1不具有这种特殊语义行为。这种特殊语义行为就是:多态性。

当我们看到表达式1时,我们看到名为f的函数被应用在了o上。我们推断只有一个名为f的函数,并且它可能不是围绕着o的标准函数中的一员。

另一方面,当我们看到表达式2时,我们看到一个名为f的消息被发送给了一个名为o的对象。我们推测可能还有其他类型的对象可以接受该消息f,因此我们不知道被调用的f具体是哪一个。其行为取决于o的类型,即f是多态的。

对多态性的这种预期是OO编程的本质。这是还原论的定义;它与OO密不可分。没有多态性的OO不是OO。OO的所有其他属性,例如封装的数据,绑定到该数据的方法,甚至继承,与表达式1的关系要比与表达式2的关系更多。

C和Pascal程序员(甚至在某种程度上甚至包括Fortran和Cobol程序员)都创建了包含封装函数和数据结构的系统。要创建和使用这种封装的结构并不一定非得需要OO语言。封装,甚至简单的继承,在此类语言中都是显而易见且自然的。(在C和Pascal中比其他更自然。)

因此,真正将OO程序与非OO程序区分开的是多态性。

您可能会说可以通过在f内部使用switch语句或if/else来实现多态。的确如此,因此我必须向OO添加一个约束。

多态机制一定不能创建从调用方到被调用方的源码依赖关系。

为了解释这一点,请再次看看上文的两个表达式。表达式1:f(o)似乎对f函数的源码有依赖。我们之所以如此推断是因为我们推断只有一个f,所以调用者必须认识被调用者。

但是,当我们看表达式2时,从o.f()我们推断出一些不同的东西。我们知道可能会有很多个f的实现,而且我们不知道真正要被调用到的是其中哪个。因此表达式2对于f函数的源码没有依赖。

具体来说,这意味着包含对函数进行多态调用的模块(源文件)绝对不能以任何方式引用包含这些函数实现的模块(源文件)。不可以有任何include或use或require或任何其它这样的声明使得一个源文件依赖另一个。

因此,我们对OO的简化定义是:

调用者的源码对于被调用者的源码不产生依赖的一种动态多态技巧。

什么是FP?

同样,我将采用还原主义。FP具有悠久的历史和传统,可追溯到软件之外的其他领域。FP范式里存在很多原理,技术,定理,哲学和概念。我将忽略所有这些内容,直接进入将FP与任何其他范式区分开的不可分割的属性。简而言之,就是:

1
当 a==b 时 f(a)==f(b)

在函数式程序中,每次调用同一个函数并给出同一个参数时,都会得到相同的结果。无论程序执行了多长时间。这叫做引用透明性。

这意味着函数f不可以更改任何影响函数f行为方式的全局状态。而且,如果我们说函数f可以代表系统中的所有函数 – 系统中的所有函数都必须是引用透明的 – 那么系统中的任何函数都无法改变任何全局状态。系统中的任何函数都无法执行任何操作,来导致系统中的另一个函数对相同的输入返回不同的值。

其更深的含义是,任何命名值都无法更改。也就是说,不能有赋值运算符。

现在,如果您仔细地考虑一下,您可能会得出这样的结论:仅由引用透明的函数组成的程序根本无法执行任何操作-因为任何有用的系统行为都会改变某些事物的状态。即使只是打印机或显示器的状态。但是,如果我们从引用透明性约束中排除硬件以及外界的任何元素,那么事实证明我们确实可以创建非常有用的系统。

诀窍当然是递归。考虑一个以state数据结构作为参数的函数。此参数包含函数工作需要的所有状态信息。该函数将创建一个新的state,里面包含更新过的值。该函数做的最后一件事就是调用它自己并把新的state作为参数传递进去。

这是函数式程序可以用来跟踪内部状态的变化而无需真正改变任何内部状态的简单技巧之一。

因此,函数式编程的简化定义为:

引用透明 – 没有重新赋值。

FP vs OO

现在OO和FP社区都要向我开炮了。还原主义不是赢得朋友的好方法。但这有时很有用。我认为有必要在似乎正在传播的FP vs OO的迷因上说两句。

显然,我选择的两个归约定义是完全正交的。多态和引用透明之间没有任何关系。它们之间没有交集。

但是正交并不意味着相互排斥(问问麦克斯韦就知道了)。建立同时使用动态多态性和引用透明性的系统是完全可能的。不仅可能,而且是可取的!

为什么是可取的?二者各自独立可取,合一仍可取!我们希望系统具有动态多态性,为了解耦。依赖关系可以跨架构边界反转。可以使用Mocks and Fakes和其他类型的Test Doubles进行测试。可以在不强制更改其他模块的情况下修改模块。这使得系统更易于更改和改进。

我们还希望系统具有引用透明性,为了可预测性。无法更改内部状态使系统更易于理解,更改和改进。它大大减少了竞态和其他并发更新问题的机会。

底线是:

没有FP vs OO。

FP和OO可以很好地合作。这两个属性都是现代系统所希望具有的。同时基于OO和FP原理构建的系统将最大限度地提高灵活性,可维护性,可测试性,简单性和健壮性。排斥一个赞成另一个只会削弱系统的结构。