實(shí)用函數(shù)式 Java (PFJ)簡(jiǎn)介
【注】本文譯自:Introduction To Pragmatic Functional Java - DZone Java (
https://dzone.com/articles/introduction-to-pragmatic-functional-java)

Pragmatic Functional Java 是一種基于函數(shù)式編程概念的現(xiàn)代、非常簡(jiǎn)潔但可讀的 Java 編碼風(fēng)格。
Pragmatic Functional Java (PFJ) 試圖定義一種新的慣用 Java 編碼風(fēng)格。編碼風(fēng)格,將完全利用當(dāng)前和即將推出的 Java 版本的所有功能,并涉及編譯器來(lái)幫助編寫簡(jiǎn)潔但可靠和可讀的代碼。
雖然這種風(fēng)格甚至可以在 Java 8 中使用,但在 Java 11 中它看起來(lái)更加簡(jiǎn)潔和簡(jiǎn)潔。它在 Java 17 中變得更具表現(xiàn)力,并受益于每個(gè)新的 Java 語(yǔ)言功能。
但 PFJ 不是免費(fèi)的午餐,它需要開(kāi)發(fā)人員的習(xí)慣和方法發(fā)生重大改變。改變習(xí)慣并不容易,傳統(tǒng)的命令式習(xí)慣尤其難以解決。
這值得么? 確實(shí)! PFJ 代碼簡(jiǎn)潔、富有表現(xiàn)力且可靠。它易于閱讀和維護(hù),并且在大多數(shù)情況下,如果代碼可以編譯 - 它可以工作!
實(shí)用函數(shù)式 Java 的元素
PFJ 源自一本精彩的?Effective Java?書(shū)籍,其中包含一些額外的概念和約定,特別是源自函數(shù)式編程(FP:Functional Programming)。請(qǐng)注意,盡管使用了 FP 概念,但 PFJ 并未嘗試強(qiáng)制執(zhí)行特定于 FP 的術(shù)語(yǔ)。(盡管對(duì)于那些有興趣進(jìn)一步探索這些概念的人,我們也提供了參考)。
PFJ專注于:
減輕心理負(fù)擔(dān)。
提高代碼可靠性。
提高長(zhǎng)期可維護(hù)性。
借助編譯器來(lái)幫助編寫正確的代碼。
讓編寫正確的代碼變得簡(jiǎn)單而自然,編寫不正確的代碼雖然仍然可能,但應(yīng)該需要付出努力。
盡管目標(biāo)雄心勃勃,但只有兩個(gè)關(guān)鍵的 PFJ 規(guī)則:
盡可能避免?null。
沒(méi)有業(yè)務(wù)異常。
下面,更詳細(xì)地探討了每個(gè)關(guān)鍵規(guī)則:
盡可能避免 null(ANAMAP 規(guī)則)
變量的可空性是特殊狀態(tài)之一。它們是眾所周知的運(yùn)行時(shí)錯(cuò)誤和樣板代碼的來(lái)源。為了消除這些問(wèn)題并表示可能丟失的值,PFJ 使用?Option<T>?容器。這涵蓋了可能出現(xiàn)此類值的所有情況 - 返回值、輸入?yún)?shù)或字段。
在某些情況下,例如出于性能或與現(xiàn)有框架兼容性的原因,類可能會(huì)在內(nèi)部使用?null。這些情況必須清楚記錄并且對(duì)類用戶不可見(jiàn),即所有類 API 都應(yīng)使用?Option<T>.。
這種方法有幾個(gè)優(yōu)點(diǎn):
可空變量在代碼中立即可見(jiàn)。無(wú)需閱讀文檔、檢查源代碼或依賴注釋。
編譯器區(qū)分可為空和不可為空的變量,并防止它們之間的錯(cuò)誤賦值。
消除了?null?檢查所需的所有樣板。
無(wú)業(yè)務(wù)異常(NBE 規(guī)則)
PFJ 僅使用異常來(lái)表示致命的、不可恢復(fù)的(技術(shù))故障的情況。此類異??赡軆H出于記錄和/或正常關(guān)閉應(yīng)用程序的目的而被攔截。不鼓勵(lì)并盡可能避免所有其他異常及其攔截。
業(yè)務(wù)異常是特殊狀態(tài)的另一種情況。為了傳播和處理業(yè)務(wù)級(jí)錯(cuò)誤,PFJ 使用?Result<T>?容器。同樣,這涵蓋了可能出現(xiàn)錯(cuò)誤的所有情況 - 返回值、輸入?yún)?shù)或字段。實(shí)踐表明,字段很少(如果有的話)需要使用這個(gè)容器。
沒(méi)有任何正當(dāng)?shù)那闆r可以使用業(yè)務(wù)級(jí)異常。與通過(guò)專用包裝方法與現(xiàn)有 Java 庫(kù)和遺留代碼交互。Result<T> 容器包含這些包裝方法的實(shí)現(xiàn)。
無(wú)業(yè)務(wù)異常規(guī)則具有以下優(yōu)點(diǎn):
可以返回錯(cuò)誤的方法在代碼中立即可見(jiàn)。 無(wú)需閱讀 文檔、檢查源代碼或分析調(diào)用樹(shù),以檢查可以拋出哪些異常以及在哪些條件下被拋出。
編譯器強(qiáng)制執(zhí)行正確的錯(cuò)誤處理和傳播。
幾乎沒(méi)有錯(cuò)誤處理和傳播的樣板。
我們可以為快樂(lè)的日子場(chǎng)景編寫代碼,并在最方便的點(diǎn)處理錯(cuò)誤 - 異常的原始意圖,這一點(diǎn)實(shí)際上從未實(shí)現(xiàn)過(guò)。
代碼保持可組合、易于閱讀和推理,在執(zhí)行流程中沒(méi)有隱藏的中斷或意外的轉(zhuǎn)換——你讀到的就是將要執(zhí)行的。
將遺留代碼轉(zhuǎn)換為 PFJ 風(fēng)格的代碼
好的,關(guān)鍵規(guī)則看起來(lái)不錯(cuò)而且很有用,但是真正的代碼會(huì)是什么樣子呢?
讓我們從一個(gè)非常典型的后端代碼開(kāi)始:
public interface UserRepository { ? ?User findById(User.Id userId);
}public interface UserProfileRepository { ? ?UserProfile findById(User.Id userId);
}public class UserService { ? ?private final UserRepository userRepository; ? ?private final UserProfileRepository userProfileRepository; ? ?public UserWithProfile getUserWithProfile(User.Id userId) {
? ? ? ?User user = userRepository.findById(userId); ? ? ? ?if (user == null) { ? ? ? ? ? ?throw UserNotFoundException("User with ID " + userId + " not found");
? ? ? ?}
? ? ? ?UserProfile details = userProfileRepository.findById(userId); ? ? ? ?return UserWithProfile.of(user, details == null ? UserProfile.defaultDetails() : details);
? ?}
}
示例開(kāi)頭的接口是為了上下文清晰而提供的。主要的興趣點(diǎn)是?getUserWithProfile?方法。我們一步一步來(lái)分析。
第一條語(yǔ)句從用戶存儲(chǔ)庫(kù)中檢索?user?變量。
由于用戶可能不存在于存儲(chǔ)庫(kù)中,因此?user?變量可能為?null。以下?null?檢查驗(yàn)證是否是這種情況,如果是,則拋出業(yè)務(wù)異常。
下一步是檢索用戶配置文件詳細(xì)信息。缺乏細(xì)節(jié)不被視為錯(cuò)誤。相反,當(dāng)缺少詳細(xì)信息時(shí),配置文件將使用默認(rèn)值。
上面的代碼有幾個(gè)問(wèn)題。首先,如果存儲(chǔ)庫(kù)中不存在值,則返回?null?從接口看并不明顯。 我們需要檢查文檔,研究實(shí)現(xiàn)或猜測(cè)這些存儲(chǔ)庫(kù)是如何工作的。
有時(shí)使用注解來(lái)提供提示,但這仍然不能保證 API 的行為。
為了解決這個(gè)問(wèn)題,讓我們將規(guī)則應(yīng)用于存儲(chǔ)庫(kù):
public interface UserRepository { ? ?Option<User> findById(User.Id userId);
}public interface UserProfileRepository { ? ?Option<UserProfile> findById(User.Id userId);
}
現(xiàn)在無(wú)需進(jìn)行任何猜測(cè) - API 明確告知可能不存在返回值。
現(xiàn)在讓我們?cè)倏纯?getUserWithProfile?方法。 要注意的第二件事是該方法可能會(huì)返回一個(gè)值或可能會(huì)引發(fā)異常。這是一個(gè)業(yè)務(wù)異常,因此我們可以應(yīng)用該規(guī)則。更改的主要目標(biāo) - 明確方法可能返回值或錯(cuò)誤的事實(shí):
public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
好的,現(xiàn)在我們已經(jīng)清理了 API,可以開(kāi)始更改代碼了。第一個(gè)變化是由 userRepository 現(xiàn)在返回?Option<User>?引起的:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
? ?Option<User> user = userRepository.findById(userId);
}
現(xiàn)在我們需要檢查用戶是否存在,如果不存在,則返回一個(gè)錯(cuò)誤。使用傳統(tǒng)的命令式方法,代碼應(yīng)該是這樣的:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) { ? ?Option<User> user = userRepository.findById(userId); ?
? ?if (user.isEmpty()) { ? ? ? ?return Result.failure(Causes.cause("User with ID " + userId + " not found"));
? ?}
}
代碼看起來(lái)不是很吸引人,但也不比原來(lái)的差,所以暫時(shí)保持原樣。
下一步是嘗試轉(zhuǎn)換剩余部分的代碼:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<User> user = userRepository.findById(userId);if (user.isEmpty()) {return Result.failure(Causes.cause("User with ID " + userId + " not found"));
}Option<UserProfile> details = userProfileRepository.findById(userId);
}
問(wèn)題來(lái)了:詳細(xì)信息和用戶存儲(chǔ)在?Option<T>?容器中,因此要組裝?UserWithProfile,我們需要以某種方式提取值。這里可能有不同的方法,例如,使用?Option.fold()?方法。生成的代碼肯定不會(huì)很漂亮,而且很可能會(huì)違反規(guī)則。
還有另一種方法 - 使用?Option<T>?是具有特殊屬性的容器這一事實(shí)。
特別是,可以使用?Option.map()?和?Option.flatMap()方法轉(zhuǎn)換?Option<T>?中的值。此外,我們知道,details?值將由存儲(chǔ)庫(kù)提供或替換為默認(rèn)值。為此,我們可以使用?Option.or()?方法從容器中提取詳細(xì)信息。讓我們?cè)囋囘@些方法:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) { ? ?Option<User> user = userRepository.findById(userId); ?
? ?if (user.isEmpty()) { ? ? ? ?return Result.failure(Causes.cause("User with ID " + userId + " not found"));
? ?}
? ?UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails()); ? ?Option<UserWithProfile> userWithProfile = ?user.map(userValue -> UserWithProfile.of(userValue, details)); ? ?return userWithProfile.toResult(Cause.cause(""));
}
現(xiàn)在我們需要編寫最后一步 - 將?userWithProfile?容器從?Option<T>?轉(zhuǎn)換為?Result<T>:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) { ? ?Option<User> user = userRepository.findById(userId); ?
? ?if (user.isEmpty()) { ? ? ? ?return Result.failure(Causes.cause("User with ID " + userId + " not found"));
? ?}
? ?UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails()); ? ?Option<UserWithProfile> userWithProfile = ?user.map(userValue -> UserWithProfile.of(userValue, details)); ? ?return userWithProfile.toResult(Cause.cause(""));
}
我們暫時(shí)將?return?語(yǔ)句中的錯(cuò)誤原因留空,然后再次查看代碼。
我們可以很容易地發(fā)現(xiàn)一個(gè)問(wèn)題:我們肯定知道?userWithProfile?總是存在 - 當(dāng)?user?不存在時(shí),上面已經(jīng)處理了這種情況。我們?cè)鯓硬拍芙鉀Q這個(gè)問(wèn)題?
請(qǐng)注意,我們可以在不檢查用戶是否存在的情況下調(diào)用?user.map()。僅當(dāng)?user?存在時(shí)才會(huì)應(yīng)用轉(zhuǎn)換,否則將被忽略。 這樣,我們可以消除?if(user.isEmpty())?檢查。讓我們?cè)趥鬟f給?user.map()?的 lambda 中移動(dòng)對(duì)?User?的?details?檢索和轉(zhuǎn)換到?UserWithProfile?中:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
? ?Option<UserWithProfile> userWithProfile = userRepository.findById(userId).map(userValue -> {
? ? ? ?UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails()); ? ? ? ?return UserWithProfile.of(userValue, details);
? ?}); ?
? ?return userWithProfile.toResult(Cause.cause(""));
}
現(xiàn)在需要更改最后一行,因?yàn)?userWithProfile?可能會(huì)缺失。該錯(cuò)誤將與以前的版本相同,因?yàn)閮H當(dāng)?userRepository.findById(userId)?返回的值缺失時(shí),userWithProfile 才會(huì)缺失:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) {
? ?Option<UserWithProfile> userWithProfile = userRepository.findById(userId).map(userValue -> {
? ? ? ?UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails()); ? ? ? ?return UserWithProfile.of(userValue, details);
? ?}); ?
? ?return userWithProfile.toResult(Causes.cause("User with ID " + userId + " not found"));
}
最后,我們可以內(nèi)聯(lián)?details?和?userWithProfile,因?yàn)樗鼈儍H在創(chuàng)建后立即使用一次:
public Result<UserWithProfile> getUserWithProfile(User.Id userId) { ? ?return userRepository.findById(userId)
? ? ? ?.map(userValue -> UserWithProfile.of(userValue, userProfileRepository.findById(userId)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .or(UserProfile.defaultDetails())))
? ? ? ?.toResult(Causes.cause("User with ID " + userId + " not found"));
}
請(qǐng)注意縮進(jìn)如何幫助將代碼分組為邏輯鏈接的部分。
讓我們來(lái)分析結(jié)果代碼:
代碼更簡(jiǎn)潔,為快樂(lè)的日子場(chǎng)景編寫,沒(méi)有明確的錯(cuò)誤或?null?檢查,沒(méi)有干擾業(yè)務(wù)邏輯
沒(méi)有簡(jiǎn)單的方法可以跳過(guò)或避免錯(cuò)誤或?null?檢查,編寫正確可靠的代碼是直接而自然的。
不太明顯的觀察:
所有類型都是自動(dòng)派生的。這簡(jiǎn)化了重構(gòu)并消除了不必要的混亂。如果需要,仍然可以添加類型。
如果在某個(gè)時(shí)候存儲(chǔ)庫(kù)將開(kāi)始返回?Result<T>?而不是?Option<T>,代碼將保持不變,除了最后一個(gè)轉(zhuǎn)換 (toResult) 將被刪除。
除了用?Option.or()?方法替換三元運(yùn)算符之外,結(jié)果代碼看起來(lái)很像如果我們將傳遞給 lambda 內(nèi)部的原始?return?語(yǔ)句中的代碼移動(dòng)到?map()?方法。
最后一個(gè)觀察對(duì)于開(kāi)始方便地編寫(閱讀通常不是問(wèn)題)PFJ 風(fēng)格的代碼非常有用。它可以改寫為以下經(jīng)驗(yàn)規(guī)則:在右側(cè)尋找值。比較一下:
User user = userRepository.findById(userId); // <-- 值在表達(dá)式左邊
和
return userRepository.findById(userId)
.map(user -> ...); // <-- 值在表達(dá)式右邊
這種有用的觀察有助于從遺留命令式代碼風(fēng)格向 PFJ 轉(zhuǎn)換。
與遺留代碼交互
不用說(shuō),現(xiàn)有代碼不遵循 PFJ 方法。它拋出異常,返回?null?等等。有時(shí)可以重新編寫此代碼以使其與 PFJ 兼容,但通常情況并非如此。對(duì)于外部庫(kù)和框架尤其如此。
調(diào)用遺留代碼
遺留代碼調(diào)用有兩個(gè)主要問(wèn)題。它們中的每一個(gè)都與違反相應(yīng)的 PFJ 規(guī)則有關(guān):
處理業(yè)務(wù)異常
Result<T>?包含一個(gè)名為?lift()?的輔助方法,它涵蓋了大多數(shù)用例。方法簽名看起來(lái)是這樣:
static <R> Result<R> lift(FN1 extends Cause, ? super Throwable> exceptionMapper, ThrowingSupplier<R> supplier)
第一個(gè)參數(shù)是將異常轉(zhuǎn)換為?Cause?實(shí)例的函數(shù)(反過(guò)來(lái),它用于在失敗情況下創(chuàng)建?Result<T>?實(shí)例)。第二個(gè)參數(shù)是 lambda,它封裝了對(duì)需要與 PFJ 兼容的實(shí)際代碼的調(diào)用。
在?Causesutility?類中提供了最簡(jiǎn)單的函數(shù),它將異常轉(zhuǎn)換為?Cause?的實(shí)例:fromThrowable()。它們可以與?Result.lift()?一起使用,如下所示:
public static Result<URI> createURI(String uri) {return Result.lift(Causes::fromThrowable, () -> URI.create(uri));
}
處理 null值返回
這種情況相當(dāng)簡(jiǎn)單 - 如果 API 可以返回?null,只需使用?Option.option()?方法將其包裝到?Option<T>?中。
提供遺留 API
有時(shí)需要允許遺留代碼調(diào)用以 PFJ 風(fēng)格編寫的代碼。特別是,當(dāng)一些較小的子系統(tǒng)轉(zhuǎn)換為 PFJ 風(fēng)格時(shí),通常會(huì)發(fā)生這種情況,但系統(tǒng)的其余部分仍然以舊風(fēng)格編寫,并且需要保留 API。最方便的方法是將實(shí)現(xiàn)拆分為兩部分——PFJ 風(fēng)格的 API 和適配器,它只將新 API 適配到舊 API。這可能是一個(gè)非常有用的簡(jiǎn)單輔助方法,如下所示:
public static <T> T unwrap(Result<T> value) { ? ?return value.fold(
? ? ? ?cause -> { throw new IllegalStateException(cause.message()); },
? ? ? ?content -> content
? ?);
}
在?Result<T>?中沒(méi)有提供隨時(shí)可用的輔助方法,原因如下:
可能有不同的用例,并且可以拋出不同類型的異常(已檢查和未檢查)。
將?Cause?轉(zhuǎn)換為不同的特定異常在很大程度上取決于特定的用例。
管理變量作用域
本節(jié)將專門介紹在編寫 PFJ 風(fēng)格代碼時(shí)出現(xiàn)的各種實(shí)際案例。
下面的示例假設(shè)使用?Result<T>,但這在很大程度上無(wú)關(guān)緊要,因?yàn)樗锌紤]因素也適用于?Option<T>。此外,示例假定示例中調(diào)用的函數(shù)被轉(zhuǎn)換為返回?Result<T>?而不是拋出異常。
嵌套作用域
函數(shù)風(fēng)格代碼大量使用 lambda 來(lái)執(zhí)行?Option<T>?和?Result<T>?容器內(nèi)的值的計(jì)算和轉(zhuǎn)換。每個(gè) lambda 都隱式地為其參數(shù)創(chuàng)建了作用域——它們可以在 lambda 主體內(nèi)部訪問(wèn),但不能在其外部訪問(wèn)。
這通常是一個(gè)有用的屬性,但對(duì)于傳統(tǒng)的命令式代碼,它很不尋常,一開(kāi)始可能會(huì)覺(jué)得不方便。幸運(yùn)的是,有一種簡(jiǎn)單的技術(shù)可以克服感知上的不便。
我們來(lái)看看下面的命令式代碼:
var value1 = function1(...); // function1() 可能招聘異常var value2 = function2(value1, ...); // function2() 可能招聘異常var value3 = function3(value1, value2, ...); // function3() 可能招聘異常
變量?value1?應(yīng)該可訪問(wèn)以調(diào)用?function2()?和?function3()。 這確實(shí)意味著直接轉(zhuǎn)換為 PFJ 樣式將不起作用:
function1(...).flatMap(value1 -> function2(value1, ...)).flatMap(value2 -> function3(value1, value2, ...)); // <-- 錯(cuò), value1 不可訪問(wèn)
為了保持值的可訪問(wèn)性,我們需要使用嵌套作用域,即嵌套調(diào)用如下:
function1(...).flatMap(value1 -> function2(value1, ...) ? ?.flatMap(value2 -> function3(value1, value2, ...)));
第二次調(diào)用?flatMap()?是針對(duì)?function2?返回的值而不是第一個(gè)?flatMap().?返回的值。通過(guò)這種方式,我們將?value1?保持在范圍內(nèi),并使?function3?可以訪問(wèn)它。
盡管可以創(chuàng)建任意深度的嵌套作用域,但通常多個(gè)嵌套作用域更難閱讀和遵循。在這種情況下,強(qiáng)烈建議將更深的范圍提取到專用函數(shù)中。
平行作用域
另一個(gè)經(jīng)常觀察到的情況是需要計(jì)算/檢索幾個(gè)獨(dú)立的值,然后進(jìn)行調(diào)用或構(gòu)建一個(gè)對(duì)象。讓我們看看下面的例子:
var value1 = function1(...); // function1() 可能招聘異常var value2 = function2(...); // function2() 可能招聘異常var value3 = function3(...); // function3() 可能招聘異常return new MyObject(value1, value2, value3);
乍一看,轉(zhuǎn)換為 PFJ 樣式可以與嵌套作用域完全相同。每個(gè)值的可見(jiàn)性將與命令式代碼相同。不幸的是,這會(huì)使范圍嵌套很深,尤其是在需要獲取許多值的情況下。
對(duì)于這種情況,Option<T>?和?Result<T>?提供了一組?all()?方法。這些方法執(zhí)行所有值的“并行”計(jì)算并返回?MapperX<...>?接口的專用版本。 這個(gè)接口只有三個(gè)方法——?id()、map()?和?flatMap()。map()?和?flatMap()?方法的工作方式與?Option<T>?和?Result<T>?中的相應(yīng)方法完全相同,只是它們接受具有不同數(shù)量參數(shù)的 lambda。讓我們來(lái)看看它在實(shí)踐中是如何工作的,并將上面的命令式代碼轉(zhuǎn)換為 PFJ 樣式:
return Result.all(
? ? ? ? ?function1(...),
? ? ? ? ?function2(...),
? ? ? ? ?function3(...)
? ? ? ?).map(MyObject::new);
除了緊湊和扁平之外,這種方法還有一些優(yōu)點(diǎn)。首先,它明確表達(dá)意圖——在使用前計(jì)算所有值。命令式代碼按順序執(zhí)行此操作,隱藏了原始意圖。第二個(gè)優(yōu)點(diǎn) - 每個(gè)值的計(jì)算是獨(dú)立的,不會(huì)將不必要的值帶入范圍。這減少了理解和推理每個(gè)函數(shù)調(diào)用所需的上下文。
替代作用域
一個(gè)不太常見(jiàn)但仍然很重要的情況是我們需要檢索一個(gè)值,但如果它不可用,那么我們使用該值的替代來(lái)源。當(dāng)有多個(gè)替代方案可用時(shí),這種情況的頻率甚至更低,而且在涉及錯(cuò)誤處理時(shí)會(huì)更加痛苦。
我們來(lái)看看下面的命令式代碼:
MyType value;try { ? ?value = function1(...);
} catch (MyException e1) { ? ?try { ? ? ? ?value = function2(...); ? ?
? ?} catch(MyException e2) { ? ? ? ?try { ? ? ? ? ? ?value = function3(...);
? ? ? ?} catch(MyException e3) {
? ? ? ? ? ?... // repeat as many times as there are alternatives
? ? ? ?}
? ?}
}
代碼是人為設(shè)計(jì)的,因?yàn)榍短装咐ǔk[藏在其他方法中。盡管如此,整體邏輯并不簡(jiǎn)單,主要是因?yàn)槌诉x擇值之外,我們還需要處理錯(cuò)誤。錯(cuò)誤處理使代碼變得混亂,并使初始意圖 - 選擇第一個(gè)可用的替代方案 - 隱藏在錯(cuò)誤處理中。
轉(zhuǎn)變?yōu)?PFJ 風(fēng)格使意圖非常清晰:
var value = Result.any(
? ? ? ?function1(...),
? ? ? ?function2(...),
? ? ? ?function3(...)
? ?);
不幸的是,這里有一個(gè)重要的區(qū)別:原始命令式代碼僅在必要時(shí)計(jì)算第二個(gè)和后續(xù)替代項(xiàng)。在某些情況下,這不是問(wèn)題,但在許多情況下,這是非常不可取的。幸運(yùn)的是,Result.any()?有一個(gè)惰性版本。使用它,我們可以重寫代碼如下:
var value = Result.any(
? ? ? ?function1(...), ? ? ? ?() -> function2(...), ? ? ? ?() -> function3(...)
? ?);
現(xiàn)在,轉(zhuǎn)換后的代碼的行為與它的命令式對(duì)應(yīng)代碼完全一樣。
Option<T>和 Result<T>的簡(jiǎn)要技術(shù)概述
這兩個(gè)容器在函數(shù)式編程術(shù)語(yǔ)中是單子(monad)。
Option<T>?是?Option/Optional/Maybe?monad 的直接實(shí)現(xiàn)。
Result<T>?是?Either<L,R>?的特意簡(jiǎn)化和專門版本:左類型是固定的,應(yīng)該實(shí)現(xiàn)?Cause?接口。專業(yè)化使 API 與?Option<T>?非常相似,并以失去通用性為代價(jià)消除了許多不必要的輸入。
這個(gè)特定的實(shí)現(xiàn)集中在兩件事上:
與現(xiàn)有 JDK 類(如?Optional<T>?和?Stream<T>)之間的互操作性
用于明確意圖表達(dá)的 API
最后一句話值得更深入的解釋。
每個(gè)容器都有幾個(gè)核心方法:
工廠方法
map()?轉(zhuǎn)換方法,轉(zhuǎn)換值但不改變特殊狀態(tài):present?Option<T>?保持?present,success Result<T> 保持?success。
flatMap()?轉(zhuǎn)換方法,除了轉(zhuǎn)換之外,還可以改變特殊狀態(tài):將?Option<T>?present?轉(zhuǎn)換為 empty 或?qū)?Result<T>?success?轉(zhuǎn)換為?failure。
fold()?方法,它同時(shí)處理兩種情況(Option<T>?的 present/empty?和?Result<T>?的?success/failure)。
除了核心方法,還有一堆輔助方法,它們?cè)诮?jīng)常觀察到的用例中很有用。
在這些方法中,有一組方法是明確設(shè)計(jì)來(lái)產(chǎn)生副作用的。
Option<T>?有以下副作用的方法:
Option<T> whenPresent(Consumer<? super T> consumer);Option<T> whenEmpty(Runnable action);Option<T> apply(Runnable emptyValConsumer, Consumer<? super T> nonEmptyValConsumer);
Result<T>?有以下副作用的方法:
Result<T> onSuccess(Consumer<T> consumer);Result<T> onSuccessDo(Runnable action);Result<T> onFailure(Consumer<? super Cause> consumer);Result<T> onFailureDo(Runnable action);Result<T> apply(Consumer<? super Cause> failureConsumer, Consumer<? super T> successConsumer);
這些方法向讀者提供了代碼處理副作用而不是轉(zhuǎn)換的提示。
其他有用的工具
除了?Option<T>?和?Result<T>?之外,PFJ 還使用了一些其他通用類。下面,將對(duì)每種方法進(jìn)行更詳細(xì)地描述。
Functions(函數(shù))
JDK 提供了許多有用的功能接口。不幸的是,通用函數(shù)的函數(shù)式接口僅限于兩個(gè)版本:?jiǎn)螀?shù)?Function<T, R>?和兩個(gè)參數(shù)?BiFunction<T, U, R>。
顯然,這在許多實(shí)際情況中是不夠的。此外,出于某種原因,這些函數(shù)的類型參數(shù)與 Java 中函數(shù)的聲明方式相反:結(jié)果類型列在最后,而在函數(shù)聲明中,它首先定義。
PFJ 為具有 1 到 9 個(gè)參數(shù)的函數(shù)使用一組一致的函數(shù)接口。 為簡(jiǎn)潔起見(jiàn),它們被稱為?FN1…FN9。到目前為止,還沒(méi)有更多參數(shù)的函數(shù)用例(通常這是代碼異味)。但如果有必要,該清單可以進(jìn)一步擴(kuò)展。
Tuples(元組)
元組是一種特殊的容器,可用于在單個(gè)變量中存儲(chǔ)多個(gè)不同類型的值。與類或記錄不同,存儲(chǔ)在其中的值沒(méi)有名稱。這使它們成為在保留類型的同時(shí)捕獲任意值集的不可或缺的工具。這個(gè)用例的一個(gè)很好的例子是?Result.all()?和?Option.all()?方法集的實(shí)現(xiàn)。
在某種意義上,元組可以被認(rèn)為是為函數(shù)調(diào)用準(zhǔn)備的一組凍結(jié)的參數(shù)。從這個(gè)角度來(lái)看,讓元組內(nèi)部值只能通過(guò)?map()?方法訪問(wèn)的決定聽(tīng)起來(lái)很合理。然而,具有 2 個(gè)參數(shù)的元組具有額外的訪問(wèn)器,可以使用?Tuple2<T1,T2>?作為各種?Pair<T1,T2>?實(shí)現(xiàn)的替代。
PFJ 使用一組一致的元組實(shí)現(xiàn),具有 0 到 9 個(gè)值。提供具有 0 和 1 值的元組以保持一致性。
結(jié)論
實(shí)用函數(shù)式 Java 是一種基于函數(shù)式編程概念的現(xiàn)代、非常簡(jiǎn)潔但可讀的 Java 編碼風(fēng)格。與傳統(tǒng)的慣用 Java 編碼風(fēng)格相比,它提供了許多好處:
PFJ 借助 Java 編譯器來(lái)幫助編寫可靠的代碼:
編譯的代碼通常是有效的
許多錯(cuò)誤從運(yùn)行時(shí)轉(zhuǎn)移到編譯時(shí)
某些類別的錯(cuò)誤,例如?NullPointerException?或未處理的異常,實(shí)際上已被消除
PFJ 顯著減少了與錯(cuò)誤傳播和處理以及?null?檢查相關(guān)的樣板代碼量
PFJ 專注于清晰表達(dá)意圖并減少心理負(fù)擔(dān)