最近,由于我把工具类看作反模式,所以被指责反对函数式编程。这是绝对错误的!我认为它们是很糟糕的反模式,因为他们与函数式编程无关。我认为其中有两个基本原因。首先,函数式编程是可声明的,然而工具类方法是命令式的。第二,函数式编程是基于lambda演算,即被传递参数的函数。从这个意义上来说,工具类方法不是函数。我会用一点时间来解释一下。
在Java中,基本上有两种被Guava、Apache Commons和其它开发库推荐使用的拙劣的工具类。***种是使用传统的类,第二种就是Java 8的lambda。现在让我们看看为什么工具类和函数式编程关系不大,以及错误观念的来源。
这就是来源于Java 1.0中Math工具类的一个典型示例:
- public class Math {
- public static double abs(double a);
- // a few dozens of other methods of the same style
- }
当你想要计算一个浮点型数字的绝对值,你可以使用如下方式:
- double x = Math.abs(3.1415926d);
这里有什么问题呢?我们需要一个函数,并且我们从 Math类中得到了结果。这个类有许多有用的内置函数,可以用于许多典型的数学运算,比如计算***值、最小值、正弦、余弦等。这是一个非常流行的概念,许 多商业化或者开源产品也是如此。自从Java出现(Math类在Java***版本被引入),这些工具类就被广泛使用。当然,在技术上没有什么不妥。相反, 他们是命令式和过程式的。我们是否在意呢?这取决于你的选择。让我们来看看他们有什么区别。
基本上有两种不同的选择,声明式和命令式。
就改变程序状态的声明来说,命令式编程的重点是描述一个程序是如何运作的。我们刚刚看到了上面一个命令式编程的例子。下面是另一个(这是一个和面向对象无关,纯粹的命令式并且程序化的代码):
- public class MyMath {
- public double f(double a, double b) {
- double max = Math.max(a, b);
- double x = Math.abs(max);
- return x;
- }
- }
就采取的一系列举措来说,声明式编程侧重于在没有规定如何做的情况下程序应该完成哪些事情。就像是Lisp中的代码,一种函数式编程语言。
- (defun f (a b) (abs (max a b)))
我们明白了什么?只是句法的不同?不是这样的。
在命令式和声明式之间有很多描述差异,但是我尽量给出自己的理解。基本上有三种角色在使用f函数的场景下相互影响:买家、包装者和消费者,让我们谈一谈下面的调用:
- public void foo() {
- double x = this.calc(5, -7);
- System.out.println("max+abs equals to " + x);
- }
- private double calc(double a, double b) {
- double x = Math.f(a, b);
- return x;
- }
这个例子中,方法calc()是一个买家,方法Math.f()是结果的包装者,方法foo()是消费者。无论使用哪种编程风格,总是有这三个参与其中,买家、包装者,和消费者。
想象一下,你是一个买家并希望购买礼物给你的女朋友或男朋友。首先会想到进一家店铺,消费50美元,让别人喷上香水打包给你,然后寄给你的朋友(回报是一枚香吻),这是命令式的风格。
第二个选项是进一家店铺,消费50美元,并得到一张礼品券,你将此券展示给你的朋友(回报是一枚香吻)。当他或者她想要得到这股芳香,他或她就会进这家店来得到它。这就是声明式风格。
看到什么区别了么?
在***个场景中,这是命令式的风格,你要求包装者(一家店铺)使用库存中的香水来打包,并作为准备好的礼品呈现给你。在第二个 场景中,这是声明式的,你最终得到了店铺的承诺,当必要的时候店铺职员会找到香水来打包礼物,并提供给需要的人。如果你的朋友从来没有进过有礼品券的这家 店,这股芳香将一直留在这家店中。
此外,你的朋友可以用这个礼品券当做这个礼品本身,就不用去这家店。他或她可能会将这张券作为礼物给其他人,或者用来交换其它礼券或者礼品。这个礼品券本身成为了一个礼品。
因此,区别就是消费者得到了什么,是用来当做礼品(命令式)还是之后可以转换成真实礼品的礼券(声明式)。
工具类,就像从JDK中的Math类或 者Apache Commons中的StringUtils类中立刻得到了准备好的礼品。然而,从Lisp中的函数和其它函数式编程中,却得到了“礼券”。比如,如果你想 调用Lisp中的求***值的方法,但只有当你真正开始使用的时候才能计算出来。
- (let (x (max 1 5))
- (print "X equals to " x))
直到输出结果打印到屏幕上,求***值的函数才会调用。当你尝试去“购买”1到5之间***值的时候,这个x就是一个返回给你的“礼券”。
但是请注意,嵌套的Java静态函不会让他们可声明化,代码仍然是命令式的,因为此时方法进行了传值。
- public class MyMath {
- public double f(double a, double b) {
- return Math.abs(Math.max(a, b));
- }
- }
你可能会说,“好吧,我明白了。但是为什么声明式的风格比命令式的更好呢?有什么大不了的呢?”我会慢慢解释的。首先让我来展示在面向对象中函数式编程中的函数和静态方法的区别。正如上面所提到的,这是工具类和函数式编程之间第二大的区别。
在函数式变成语言中,你可以这么做:
- (defun foo (x) (x 5))
然后,你可以调用这个x:
- (defun bar (x) (+ x 1)) // defining function bar
- (print (foo bar)) // passing bar as an argument to foo
就函数式编程而 言,Java中的静态方法不是函数。你不能用一个静态方法做这样的事。你不能将一个静态方法当做参数传递给其他方法。基本上静态方法是生产者,或者简单地 说,Java由唯一的名字所声明。唯一的方法就是调用一个程序并且传递所有必要的参数给它。这个程序将会计算出结果并立即返回给调用者。
现在,我们来到了最终的问题上,我能听到你在问:“好吧,工具类不是函数式编程,但是他们看起来很像函数式编程,他们运行的很快,并且使用很方便。为什么不用他们?为什么当20年的Java历史证明了工具类是每一个Java开发者的主要手段的时候,又要力求***?”
除了面向对象的,这点我经常受指责,这里有一些实际的原因(顺便说一句,我推崇面向对象)。
可测试性。在工具类中调用静态方法是硬编码式的依赖,它不能因为测试的需要而被打断。如果你的类正在调用FileUtils.readFile(),除非我的磁盘上有一个实际的文件,否则我无法测试。
效率。工具类,由于其命令式的性质,比可替代的声明式更加低效。即使当他们不是必要使用 的时候,他们也盲目地进行所有的计算,处理资源。而不是返回一个期望值来分隔字符串chunks、StringUtils.split()可以立即打断 它。同时,这也打破了所有可能的chunks,即使“买家”仅仅需要***个。
可读性。工具类往往 是庞大的(尝试从Apache Commons阅读StringUtils或者FileUtils的源码)。关注点分离可以使得面向对象如此优雅,但这些想法在工具类中是没有的。他们尽 量把所有可能的程序放进一个.java文件,这导致当它的大小超过了许多静态方法的时候是极难维护的。
***,我要重申一下:工具类与函数编程无关。他们仅仅是静态方法的包装,是命令式的程序。无论你要声明他们多少次,他们有多渺小,都要尽量远离他们而去使用可靠、健壮的对象。