Utility Classes Have Nothing to Do With Functional Programming

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

我最近被指责反对函数式编程,因为我称实用类为反模式。这完全是错误的!嗯,我确实认为它们是一种可怕的反模式,但它们与函数式编程没有任何关系。我认为有两个基本原因。首先,函数式编程是声明式的,而实用类方法是命令式的。其次,函数式编程是基于λ演算的,其中函数可以分配给变量。实用类方法并不是这个意义上的函数。我会在一分钟内解码这些陈述。

在Java中,基本上有两种有效的替代方案,来替代这些由Guava、Apache Commons等积极推广的丑陋实用类。第一种是使用传统类,第二种是Java 8的lambda表达式。现在让我们看看为什么实用类与函数式编程相去甚远,以及这个误解从何而来。

这是一个典型的Java 1.0实用类Math的例子:

这是你想要计算浮点数的绝对值时如何使用它的方式:

它有什么问题?我们需要一个函数,而我们从类Math中获取它。这个类内部有许多有用的函数,可以用于许多典型的数学运算,比如计算最大值、最小值、正弦、余弦等等。这是一个非常流行的概念;只要看看任何商业或开源产品就知道了。自从Java被发明以来,这些实用类就被广泛使用(Math类在Java的第一个版本中引入)。嗯,从技术上讲没有什么问题。代码可以正常工作。但这并不是面向对象的编程方式。相反,它是命令式和过程式的。我们在乎吗?嗯,这取决于你自己来决定。让我们来看看它们之间的区别。

基本上有两种不同的方法:声明式和命令式。

命令式编程侧重于以改变程序状态的语句描述程序的运行方式。我们刚刚看到了一个命令式编程的示例。这里是另一个例子(这是纯粹的命令式/过程式编程,与面向对象编程无关):

声明式编程 关注于程序应完成的目标,而不规定采取的操作顺序。以下是同一段代码在Lisp中(一种函数式编程语言)的写法:

有何问题?只是语法上的差异吗?事实并非如此。

关于命令式和声明式风格之间的区别,有许多定义,但我将尝试给出我自己的定义。在与这个f函数/方法交互的场景中,基本上有三个角色:一个购买者,一个结果的打包者,以及一个结果的消费者。假设我像这样调用这个函数:

在这里,方法 calc() 是一个买家,方法 Math.f() 是结果的打包者,方法 foo() 是一个消费者。无论使用哪种编程风格,这三个角色始终参与其中:买家、打包者和消费者。

想象一下,你是一个买家,想要为你的(女朋友男朋友)购买一份礼物。第一种选择是去商店,支付50美元,让他们为你打包那瓶香水,然后将其送给朋友(并得到一个吻作为回报)。这是一种命令式风格。

第二种选择是去商店,支付50美元,并获得一张礼品卡。然后,你将这张卡送给朋友(并得到一个吻作为回报)。当他或她决定将其兑换成香水时,他或她会去商店并获得它。这是一种声明式风格。

在第一种情况下,你强迫包装者(一家美容店)去找到那瓶香水存货,将其包装好,并作为一个可直接使用的产品呈现给你。在第二种情况下,也就是陈述语气,你只是从店铺得到一个承诺,即在必要时,工作人员将会找到香水存货,将其包装好,并提供给需要的人。如果你的朋友从未用过礼品卡去店里购物,香水将会保持现货。

此外,你的朋友可以将礼品卡作为产品本身使用,而无需去店里。他或她可以将其作为礼物赠送给别人,或者仅仅用它来交换另一张卡或其他产品。礼品卡本身也成为了一个产品!

因此,区别在于消费者得到的东西——要么是一个可直接使用的产品(命令式),要么是一个产品的凭证,可以在以后转换为实际的产品(陈述式)。

实用类,如JDK中的Math或Apache Commons中的StringUtils,会立即返回可直接使用的产品,而Lisp和其他函数式语言中的函数则返回“凭证”。例如,如果在Lisp中调用max函数,只有在你实际开始使用它时,才会计算出两个数字之间的最大值。

在这个print开始向屏幕输出字符之前,函数max不会被调用。这个x是当你尝试“购买”15之间的最大值时返回给你的“凭证”。

然而,请注意,将一个Java静态函数嵌套到另一个函数中并不使它们成为声明性函数。代码仍然是命令式的,因为它的执行立即产生结果。

“好的,”你可能会说,“我明白了,但是为什么声明式风格比命令式风格更好?这有什么了不起的?”我会解释的。首先,让我先展示函数式编程中的函数与面向对象编程中的静态方法之间的区别。如上所述,这是实用类和函数式编程之间的第二个重大区别。

在任何函数式编程语言中,你可以这样做:

然后,稍后,您可以调用x

在Java中,静态方法不是函数式编程中的“函数”。你无法像这样使用静态方法。你不能将静态方法作为参数传递给另一个方法。基本上,静态方法是过程或者简单来说,是以唯一名称分组的Java语句。访问它们的唯一方法是调用一个过程并将所有必要的参数传递给它。该过程将计算某些内容并返回一个立即可以使用的结果。

现在我们来谈谈最后一个问题,我能听到你正在问:“好吧,实用类不是函数式编程,但它们看起来像函数式编程,它们工作速度非常快,使用起来非常简单。为什么不使用它们?为什么要追求完美,当Java的20年历史证明实用类是每个Java开发者的主要工具?”

除了我经常被指责为面向对象编程教条主义之外,还有一些非常实际的原因(顺便说一下,我是面向对象编程教条主义者):

  • 效率。由于其命令式的性质,实用类远不如声明式的替代方案高效。它们在此刻直接进行所有计算,即使此时还不需要,也会占用处理器资源。与返回一个将字符串分解成块的承诺不同,StringUtils.split() 立即将其分解。而且,它会将其分解为所有可能的块,即使只有第一个块是“买方”所需的。

  • 可读性。工具类往往非常庞大(尝试阅读Apache Commons的StringUtilsFileUtils的源代码)。关注点分离的整个理念,使得面向对象编程如此美丽,在工具类中却缺失了。它们只是将所有可能的过程放入一个巨大的.java文件中,当超过十几个静态方法时,变得绝对难以维护。

总之,让我重申一下:实用类与函数式编程无关。它们只是一堆静态方法的集合,这些方法是命令式过程。尽量远离它们,并且无论你需要声明多少个,无论它们多么小,都使用可靠、连贯的对象。

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-16 at 15:37

sixnines availability badge   GitHub stars