使用函數式編程可以減少代碼重復,使代碼更易于理解。
JAVA編程語言以其面向對象的特性而聞名,但也因其冗長和繁瑣的異常處理機制而而廣受批評。當Java語言在1.8版本引入函數式編程能力時,人們并沒有馬上理解到這如何給程序員提供幫助。
本文給大家講解一個示例,以說明函數式編程如何提高代碼的重用性和可讀性。
1 問題
為了實現功能,第三方編寫的通信客戶端使用了依賴注入和注釋。然而,該客戶端存在一些問題,例如拋出已檢查異常、缺乏日志記錄和重試能力。因此,我們需要對該客戶端的功能進行封裝,以添加重試功能、日志記錄和良好的異常處理。
如果沒有函數式編程,那么程序需要創建一個外觀,將每個客戶端功能都包裝在委托中,并添加日志記錄、異常處理和通信重試的邏輯??蛻舳斯灿?0個需要封裝的函數,這將導致幾乎完全相同的50個副本的代碼,僅有調用的客戶端函數和傳遞的數據類型有所不同。這樣會帶來大量的重復代碼問題。
為了解決這個問題,要使用函數式編程的方式對該客戶端功能進行封裝,以實現重試能力、日志記錄和良好的異常處理。
2 使用函數式編程解決問題
解決方案是添加1或2個專門用于處理異常、執行日志記錄和實現重試循環的方法。但是,它們需要調用通信客戶端中的特定函數。
使用函數式編程,方法可以將函數作為其參數接收。該方法不需要在設計時知道哪個函數。Java(因為這與其他編程語言不同)所要求的是函數具有預期的簽名。
在我們的情況下,我們需要2種變體:BiFunction簽名和BiConsumer簽名。區別在于BiFunction返回一個值,而BiConsumer則不返回。
2.1 示例代碼:委托
public final ActionResult<List<Order>>
downloadOrders() {
return get(
".downloadOrders()",
(
client,
apiKey
) -> client.downloadOrders(apiKey.toString())
);
}
上面的示例沒有顯示錯誤處理、日志記錄和重試循環。這是由get(String, BiFunction)方法執行的。因此,我們不需要50個重復的錯誤處理、日志記錄和重試循環,而是有50個易于理解的委托。
get(String, BiFunction)方法回調傳入的BiFunction,我們在上面看到了這一點:
(
client,
apiKey
) -> client.downloadOrders(apiKey.toString())
我們可能會從JavaScript中認識到這一點。語法是一個BiFunction的lambda表示法:它接受2個參數,可能做一些事情,并返回一些東西。它是一個回調函數,根據需要即時創建,并且因為它沒有名稱,所以它保持匿名(并由編譯器分配標識)。它的參數client和apiKey在方法get(String, BiFunction)內生成,并在運行時傳回回調函數中。它看起來像這樣:
2.2 示例代碼:接受函數作為參數的包裝器
private final <T>
ActionResult<T>
get(
final String caller,
final BiFunction<
Client,
ApiKey,
T
> callback
) {
final Client client = Client.newInstance(caller);
final ApiKey key = this.getKey();
final MutableList<Exception> errors = Lists.mutable.empty();
boolean mustRetry = true;
for (
int retryCount = 0;
mustRetry && retryCount < MAX_RETRIES;
retryCount += 1
) {
try {
return new ActionResult<T>(
callback.Apply(
client,
apiKey
)
).with(errors);
} catch (Exception ex) {
mustRetry = mustRetry(ex);
if (mustRetry) {
try {
TimeUnit.SECONDS.wAIt(1 << (retryCount + 1));
} catch (InterruptedException ie) {
errors.add(ie);
}
} else {
errors.add(ex);
}
}
}
return ActionResult.<T>empty()
.with(errors);
}
在那段代碼中,BiFunction通過聲明進行回調:
callback.apply(
client,
apiKey
)
請注意,get(String, BiFunction)不知道回調返回的數據類型。在像Java這樣的強類型和顯式類型編程語言中,通常不可能。直到泛型引入Java編程語言之前,這是不可能的。這就是為什么代碼中存在:它是回調返回的數據類型的占位符。
請注意,調用站點也沒有指定返回值的數據類型。相反,它由客戶端函數的返回值和委托上指定的返回數據類型隱含。如果它們不匹配,編譯器將發出警告,保持數據類型良好且檢查過。
3 總結
-
壞的抽象是面向對象編程中許多問題的根源。我們希望將目前的兩個異常處理和重試循環包裝器減少到一個包裝器,但是由于一個接受BiFunction回調,另一個接受BiConsumer回調,還沒有找到合適的方法。因為從概念上講,它們執行不同的操作:發送和接收。使用相同的包裝器可能會破壞這種概念上的區別??傊?,對于現在來說值得高興的是避免了重復使用50個包裝器的情況。
-
當然,有些優秀的編譯器可能會檢測到代碼重復,并采取一些技巧將它們減少為具有不同回調的單個包裝器。然而,這并非我們當前關注的重點。我們的目標是消除重復代碼并提高代碼的可讀性。
-
從理論上講,使用這個包裝器和函數委托可能會導致程序變慢。然而,根據經驗,由于與遠程通信伙伴進行通信時遇到的網絡延遲,潛在的幾毫秒延遲相對微不足道。盡管如此,如果在您的環境中這導致了明顯的性能下降,請測量吞吐量并進行相應的調整。
-
靜態代碼分析工具若能提供有關消除重復代碼的建議,將對代碼優化非常有益。這樣的工具可以幫助開發人員識別和消除重復的代碼段,從而提高代碼的可讀性、可維護性和性能。






