最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

實(shí)用函數(shù)式 Java (PFJ)簡(jiǎn)介

2021-11-05 18:45 作者:信碼由韁  | 我要投稿

【注】本文譯自: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)


實(shí)用函數(shù)式 Java (PFJ)簡(jiǎn)介的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
阿瓦提县| 通山县| 绵竹市| 高尔夫| 杂多县| 融水| 沅陵县| 政和县| 瑞安市| 宁阳县| 电白县| 武威市| 杭州市| 阜宁县| 错那县| 溧阳市| 宣化县| 衡阳县| 瑞昌市| 阳春市| 洛扎县| 文成县| 南雄市| 都安| 高阳县| 姜堰市| 长顺县| 万安县| 招远市| 葫芦岛市| 教育| 雅江县| 炎陵县| 阿城市| 金平| 哈密市| 磐安县| 和顺县| 大悟县| 德惠市| 华阴市|