工具類與函數編程毫不相干
最近,由于我把工具類看作反模式,所以被 指責反對 函數式編程。這是絕對錯誤的! 我認為它們是很糟糕的反模式,因為他們與函數式編程無關。我認為其中有兩個基本原因。首先,函數式編程是可聲明的,然而工具類方法是命令式的。第二,函數式編程是基于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文件,這導致當它的大小超過了許多靜態方法的時候是極難維護的。
最后,我要重申一下:工具類與函數編程無關。他們僅僅是靜態方法的包裝,是命令式的程序。無論你要聲明他們多少次,他們有多渺小,都要盡量遠離他們而去使用可靠、健壯的對象。
原文鏈接: yegor256 翻譯: ImportNew.com - treesouth譯文鏈接: http://www.importnew.com/15747.html