使用函數(shù)式編程可以減少代碼重復(fù),使代碼更易于理解。
JAVA編程語(yǔ)言以其面向?qū)ο蟮奶匦远劽惨蚱淙唛L(zhǎng)和繁瑣的異常處理機(jī)制而而廣受批評(píng)。當(dāng)Java語(yǔ)言在1.8版本引入函數(shù)式編程能力時(shí),人們并沒(méi)有馬上理解到這如何給程序員提供幫助。
本文給大家講解一個(gè)示例,以說(shuō)明函數(shù)式編程如何提高代碼的重用性和可讀性。
1 問(wèn)題
為了實(shí)現(xiàn)功能,第三方編寫(xiě)的通信客戶(hù)端使用了依賴(lài)注入和注釋。然而,該客戶(hù)端存在一些問(wèn)題,例如拋出已檢查異常、缺乏日志記錄和重試能力。因此,我們需要對(duì)該客戶(hù)端的功能進(jìn)行封裝,以添加重試功能、日志記錄和良好的異常處理。
如果沒(méi)有函數(shù)式編程,那么程序需要?jiǎng)?chuàng)建一個(gè)外觀,將每個(gè)客戶(hù)端功能都包裝在委托中,并添加日志記錄、異常處理和通信重試的邏輯??蛻?hù)端共有50個(gè)需要封裝的函數(shù),這將導(dǎo)致幾乎完全相同的50個(gè)副本的代碼,僅有調(diào)用的客戶(hù)端函數(shù)和傳遞的數(shù)據(jù)類(lèi)型有所不同。這樣會(huì)帶來(lái)大量的重復(fù)代碼問(wèn)題。
為了解決這個(gè)問(wèn)題,要使用函數(shù)式編程的方式對(duì)該客戶(hù)端功能進(jìn)行封裝,以實(shí)現(xiàn)重試能力、日志記錄和良好的異常處理。
2 使用函數(shù)式編程解決問(wèn)題
解決方案是添加1或2個(gè)專(zhuān)門(mén)用于處理異常、執(zhí)行日志記錄和實(shí)現(xiàn)重試循環(huán)的方法。但是,它們需要調(diào)用通信客戶(hù)端中的特定函數(shù)。
使用函數(shù)式編程,方法可以將函數(shù)作為其參數(shù)接收。該方法不需要在設(shè)計(jì)時(shí)知道哪個(gè)函數(shù)。Java(因?yàn)檫@與其他編程語(yǔ)言不同)所要求的是函數(shù)具有預(yù)期的簽名。
在我們的情況下,我們需要2種變體:BiFunction簽名和BiConsumer簽名。區(qū)別在于BiFunction返回一個(gè)值,而B(niǎo)iConsumer則不返回。
2.1 示例代碼:委托
public final ActionResult<List<Order>>
downloadOrders() {
return get(
".downloadOrders()",
(
client,
apiKey
) -> client.downloadOrders(apiKey.toString())
);
}
上面的示例沒(méi)有顯示錯(cuò)誤處理、日志記錄和重試循環(huán)。這是由get(String, BiFunction)方法執(zhí)行的。因此,我們不需要50個(gè)重復(fù)的錯(cuò)誤處理、日志記錄和重試循環(huán),而是有50個(gè)易于理解的委托。
get(String, BiFunction)方法回調(diào)傳入的BiFunction,我們?cè)谏厦婵吹搅诉@一點(diǎn):
(
client,
apiKey
) -> client.downloadOrders(apiKey.toString())
我們可能會(huì)從JavaScript中認(rèn)識(shí)到這一點(diǎn)。語(yǔ)法是一個(gè)BiFunction的lambda表示法:它接受2個(gè)參數(shù),可能做一些事情,并返回一些東西。它是一個(gè)回調(diào)函數(shù),根據(jù)需要即時(shí)創(chuàng)建,并且因?yàn)樗鼪](méi)有名稱(chēng),所以它保持匿名(并由編譯器分配標(biāo)識(shí))。它的參數(shù)client和apiKey在方法get(String, BiFunction)內(nèi)生成,并在運(yùn)行時(shí)傳回回調(diào)函數(shù)中。它看起來(lái)像這樣:
2.2 示例代碼:接受函數(shù)作為參數(shù)的包裝器
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通過(guò)聲明進(jìn)行回調(diào):
callback.apply(
client,
apiKey
)
請(qǐng)注意,get(String, BiFunction)不知道回調(diào)返回的數(shù)據(jù)類(lèi)型。在像Java這樣的強(qiáng)類(lèi)型和顯式類(lèi)型編程語(yǔ)言中,通常不可能。直到泛型引入Java編程語(yǔ)言之前,這是不可能的。這就是為什么代碼中存在:它是回調(diào)返回的數(shù)據(jù)類(lèi)型的占位符。
請(qǐng)注意,調(diào)用站點(diǎn)也沒(méi)有指定返回值的數(shù)據(jù)類(lèi)型。相反,它由客戶(hù)端函數(shù)的返回值和委托上指定的返回?cái)?shù)據(jù)類(lèi)型隱含。如果它們不匹配,編譯器將發(fā)出警告,保持?jǐn)?shù)據(jù)類(lèi)型良好且檢查過(guò)。
3 總結(jié)
-
壞的抽象是面向?qū)ο缶幊讨性S多問(wèn)題的根源。我們希望將目前的兩個(gè)異常處理和重試循環(huán)包裝器減少到一個(gè)包裝器,但是由于一個(gè)接受BiFunction回調(diào),另一個(gè)接受BiConsumer回調(diào),還沒(méi)有找到合適的方法。因?yàn)閺母拍钌现v,它們執(zhí)行不同的操作:發(fā)送和接收。使用相同的包裝器可能會(huì)破壞這種概念上的區(qū)別。總之,對(duì)于現(xiàn)在來(lái)說(shuō)值得高興的是避免了重復(fù)使用50個(gè)包裝器的情況。
-
當(dāng)然,有些優(yōu)秀的編譯器可能會(huì)檢測(cè)到代碼重復(fù),并采取一些技巧將它們減少為具有不同回調(diào)的單個(gè)包裝器。然而,這并非我們當(dāng)前關(guān)注的重點(diǎn)。我們的目標(biāo)是消除重復(fù)代碼并提高代碼的可讀性。
-
從理論上講,使用這個(gè)包裝器和函數(shù)委托可能會(huì)導(dǎo)致程序變慢。然而,根據(jù)經(jīng)驗(yàn),由于與遠(yuǎn)程通信伙伴進(jìn)行通信時(shí)遇到的網(wǎng)絡(luò)延遲,潛在的幾毫秒延遲相對(duì)微不足道。盡管如此,如果在您的環(huán)境中這導(dǎo)致了明顯的性能下降,請(qǐng)測(cè)量吞吐量并進(jìn)行相應(yīng)的調(diào)整。
-
靜態(tài)代碼分析工具若能提供有關(guān)消除重復(fù)代碼的建議,將對(duì)代碼優(yōu)化非常有益。這樣的工具可以幫助開(kāi)發(fā)人員識(shí)別和消除重復(fù)的代碼段,從而提高代碼的可讀性、可維護(hù)性和性能。