工具類與函數(shù)編程毫不相干
最近,由于我把工具類看作反模式,所以被指責反對函數(shù)式編程。這是絕對錯誤的!我認為它們是很糟糕的反模式,因為他們與函數(shù)式編程無關。我認為其中有兩個基本原因。首先,函數(shù)式編程是可聲明的,然而工具類方法是命令式的。第二,函數(shù)式編程是基于lambda演算,即被傳遞參數(shù)的函數(shù)。從這個意義上來說,工具類方法不是函數(shù)。我會用一點時間來解釋一下。
在Java中,基本上有兩種被Guava、Apache Commons和其它開發(fā)庫推薦使用的拙劣的工具類。***種是使用傳統(tǒng)的類,第二種就是Java 8的lambda?,F(xiàn)在讓我們看看為什么工具類和函數(shù)式編程關系不大,以及錯誤觀念的來源。
這就是來源于Java 1.0中Math工具類的一個典型示例:
- public class Math {
- public static double abs(double a);
- // a few dozens of other methods of the same style
- }
當你想要計算一個浮點型數(shù)字的絕對值,你可以使用如下方式:
- double x = Math.abs(3.1415926d);
這里有什么問題呢?我們需要一個函數(shù),并且我們從 Math類中得到了結果。這個類有許多有用的內置函數(shù),可以用于許多典型的數(shù)學運算,比如計算***值、最小值、正弦、余弦等。這是一個非常流行的概念,許 多商業(yè)化或者開源產品也是如此。自從Java出現(xiàn)(Math類在Java***版本被引入),這些工具類就被廣泛使用。當然,在技術上沒有什么不妥。相反, 他們是命令式和過程式的。我們是否在意呢?這取決于你的選擇。讓我們來看看他們有什么區(qū)別。
基本上有兩種不同的選擇,聲明式和命令式。
就改變程序狀態(tài)的聲明來說,命令式編程的重點是描述一個程序是如何運作的。我們剛剛看到了上面一個命令式編程的例子。下面是另一個(這是一個和面向對象無關,純粹的命令式并且程序化的代碼):
- public class MyMath {
- public double f(double a, double b) {
- double max = Math.max(a, b);
- double x = Math.abs(max);
- return x;
- }
- }
就采取的一系列舉措來說,聲明式編程側重于在沒有規(guī)定如何做的情況下程序應該完成哪些事情。就像是Lisp中的代碼,一種函數(shù)式編程語言。
- (defun f (a b) (abs (max a b)))
我們明白了什么?只是句法的不同?不是這樣的。
在命令式和聲明式之間有很多描述差異,但是我盡量給出自己的理解?;旧嫌腥N角色在使用f函數(shù)的場景下相互影響:買家、包裝者和消費者,讓我們談一談下面的調用:
- 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美元,并得到一張禮品券,你將此券展示給你的朋友(回報是一枚香吻)。當他或者她想要得到這股芳香,他或她就會進這家店來得到它。這就是聲明式風格。
看到什么區(qū)別了么?
在***個場景中,這是命令式的風格,你要求包裝者(一家店鋪)使用庫存中的香水來打包,并作為準備好的禮品呈現(xiàn)給你。在第二個 場景中,這是聲明式的,你最終得到了店鋪的承諾,當必要的時候店鋪職員會找到香水來打包禮物,并提供給需要的人。如果你的朋友從來沒有進過有禮品券的這家 店,這股芳香將一直留在這家店中。
此外,你的朋友可以用這個禮品券當做這個禮品本身,就不用去這家店。他或她可能會將這張券作為禮物給其他人,或者用來交換其它禮券或者禮品。這個禮品券本身成為了一個禮品。
因此,區(qū)別就是消費者得到了什么,是用來當做禮品(命令式)還是之后可以轉換成真實禮品的禮券(聲明式)。
工具類,就像從JDK中的Math類或 者Apache Commons中的StringUtils類中立刻得到了準備好的禮品。然而,從Lisp中的函數(shù)和其它函數(shù)式編程中,卻得到了“禮券”。比如,如果你想 調用Lisp中的求***值的方法,但只有當你真正開始使用的時候才能計算出來。
- (let (x (max 1 5))
- (print "X equals to " x))
直到輸出結果打印到屏幕上,求***值的函數(shù)才會調用。當你嘗試去“購買”1到5之間***值的時候,這個x就是一個返回給你的“禮券”。
但是請注意,嵌套的Java靜態(tài)函不會讓他們可聲明化,代碼仍然是命令式的,因為此時方法進行了傳值。
- public class MyMath {
- public double f(double a, double b) {
- return Math.abs(Math.max(a, b));
- }
- }
你可能會說,“好吧,我明白了。但是為什么聲明式的風格比命令式的更好呢?有什么大不了的呢?”我會慢慢解釋的。首先讓我來展示在面向對象中函數(shù)式編程中的函數(shù)和靜態(tài)方法的區(qū)別。正如上面所提到的,這是工具類和函數(shù)式編程之間第二大的區(qū)別。
在函數(shù)式變成語言中,你可以這么做:
- (defun foo (x) (x 5))
然后,你可以調用這個x:
- (defun bar (x) (+ x 1)) // defining function bar
- (print (foo bar)) // passing bar as an argument to foo
就函數(shù)式編程而 言,Java中的靜態(tài)方法不是函數(shù)。你不能用一個靜態(tài)方法做這樣的事。你不能將一個靜態(tài)方法當做參數(shù)傳遞給其他方法。基本上靜態(tài)方法是生產者,或者簡單地 說,Java由唯一的名字所聲明。唯一的方法就是調用一個程序并且傳遞所有必要的參數(shù)給它。這個程序將會計算出結果并立即返回給調用者。
現(xiàn)在,我們來到了最終的問題上,我能聽到你在問:“好吧,工具類不是函數(shù)式編程,但是他們看起來很像函數(shù)式編程,他們運行的很快,并且使用很方便。為什么不用他們?為什么當20年的Java歷史證明了工具類是每一個Java開發(fā)者的主要手段的時候,又要力求***?”
除了面向對象的,這點我經常受指責,這里有一些實際的原因(順便說一句,我推崇面向對象)。
可測試性。在工具類中調用靜態(tài)方法是硬編碼式的依賴,它不能因為測試的需要而被打斷。如果你的類正在調用FileUtils.readFile(),除非我的磁盤上有一個實際的文件,否則我無法測試。
效率。工具類,由于其命令式的性質,比可替代的聲明式更加低效。即使當他們不是必要使用 的時候,他們也盲目地進行所有的計算,處理資源。而不是返回一個期望值來分隔字符串chunks、StringUtils.split()可以立即打斷 它。同時,這也打破了所有可能的chunks,即使“買家”僅僅需要***個。
可讀性。工具類往往 是龐大的(嘗試從Apache Commons閱讀StringUtils或者FileUtils的源碼)。關注點分離可以使得面向對象如此優(yōu)雅,但這些想法在工具類中是沒有的。他們盡 量把所有可能的程序放進一個.java文件,這導致當它的大小超過了許多靜態(tài)方法的時候是極難維護的。
***,我要重申一下:工具類與函數(shù)編程無關。他們僅僅是靜態(tài)方法的包裝,是命令式的程序。無論你要聲明他們多少次,他們有多渺小,都要盡量遠離他們而去使用可靠、健壯的對象。