Java 設(shè)計(jì)模式 Monads 的美麗世界

【注】本文譯自:Beautiful world of Monad - Dev Community(https://dev.to/siy/beautiful-world-of-mondas-2cd6)

讓我從免責(zé)聲明開始。從函數(shù)式編程的角度來看,下面的解釋絕不是精確的或絕對(duì)準(zhǔn)確的。相反,我將重點(diǎn)解釋的清晰和簡(jiǎn)單性上,以便讓盡可能多的 Java 開發(fā)人員進(jìn)入這個(gè)美麗的世界。
幾年前,當(dāng)我開始深入研究函數(shù)式編程時(shí),我很快發(fā)現(xiàn)有大量的信息,但對(duì)于幾乎完全具有命令式背景的普通 Java 開發(fā)人員來說,幾乎無法理解。如今,情況正在慢慢改變。例如,有很多文章解釋了例如基本的 FP 概念(參考: [實(shí)用函數(shù)式 Java (PFJ)簡(jiǎn)介]https://icodewalker.com/blog/172/)以及它們?nèi)绾芜m用于 Java?;蚪忉屓绾握_使用 Java 流的文章。但是 Monads 仍然不在這些文章的重點(diǎn)之外。我不知道為什么會(huì)發(fā)生這種情況,但我會(huì)努力填補(bǔ)這個(gè)空白。
那么,Monad 是什么?
Monad 是……一種設(shè)計(jì)模式。就這么簡(jiǎn)單。這種設(shè)計(jì)模式由兩部分組成:
Monad 是一個(gè)值的容器。對(duì)于每個(gè) Monad,都有一些方法可以將值包裝到 Monad 中。
Monad 為內(nèi)部包含的值實(shí)現(xiàn)了“控制反轉(zhuǎn)”。為了實(shí)現(xiàn)這一點(diǎn),Monad 提供了接受函數(shù)的方法。這些函數(shù)接受與 Monad 中存儲(chǔ)的類型相同的值,并返回轉(zhuǎn)換后的值。轉(zhuǎn)換后的值被包裝到與源值相同的 Monad 中。
為了理解模式的第二部分,我們可以看看 Monad 的接口:
interface Monad<T> {
? ? <R> Monad<R> map(Function<T, R> mapper);
? ? <R> Monad<R> flatMap(Function<T, Monad<R>> mapper);
}
當(dāng)然,特定的 Monad 通常有更豐富的接口,但這兩個(gè)方法絕對(duì)應(yīng)該存在。
乍一看,接受函數(shù)而不是訪問值并沒有太大區(qū)別。事實(shí)上,這使 Monad 能夠完全控制如何以及何時(shí)應(yīng)用轉(zhuǎn)換功能。當(dāng)您調(diào)用 getter 時(shí),您希望立即獲得值。在 Monad 轉(zhuǎn)換的情況下可以立即應(yīng)用或根本不應(yīng)用,或者它的應(yīng)用可以延遲。缺乏對(duì)內(nèi)部值的直接訪問使 monad 能夠表示甚至尚不可用的值!
下面我將展示一些 Monad 的例子以及它們可以解決哪些問題。
Monad?缺失值或 Optional/Maybe 的場(chǎng)景
這個(gè) Monad 有很多名字——Maybe、Option、Optional。最后一個(gè)聽起來很熟悉,不是嗎? 好吧,因?yàn)?Java 8 Optional 是 Java 平臺(tái)的一部分。
不幸的是,Java Optional 實(shí)現(xiàn)過于尊崇傳統(tǒng)的命令式方法,這使得它的用處不大。特別是 Optional 允許應(yīng)用程序使用 .get() 方法獲取值。如果缺少值,甚至?xí)伋?NPE。因此,Optional 的用法通常僅限于表示返回潛在的缺失值,盡管這只是潛在用法的一小部分。
也許 Monad 的目的是表示可能會(huì)丟失的值。傳統(tǒng)上,Java 中的這個(gè)角色是為 null 保留的。不幸的是,這會(huì)導(dǎo)致許多不同的問題,包括著名的 NullPointerException。
例如,如果您期望某些參數(shù)或某些返回值可以為 null,則應(yīng)該在使用前檢查它:
public UserProfileResponse getUserProfileHandler(final User.Id userId) {
? ? final User user = userService.findById(userId);
? ? if (user == null) {
? ? return UserProfileResponse.error(USER_NOT_FOUND);
? ? }
? ?
? ? final UserProfileDetails details = userProfileService.findById(userId);
? ?
? ? if (details == null) {
? ? return UserProfileResponse.of(user, UserProfileDetails.defaultDetails());
? ? }
? ?
? ? return UserProfileResponse.of(user, details);
}
看起來熟悉嗎?當(dāng)然了。
讓我們看看 Option Monad 如何改變這一點(diǎn)(為簡(jiǎn)潔起見,使用一個(gè)靜態(tài)導(dǎo)入):
? ? public UserProfileResponse getUserProfileHandler(final User.Id userId) {
? ? ? ? return ofNullable(userService.findById(userId))
? ? ? ? ? ? ? ? .map(user -> UserProfileResponse.of(user,
? ? ? ? ? ? ? ? ? ? ? ? ofNullable(userProfileService.findById(userId)).orElseGet(UserProfileDetails::defaultDetails)))
? ? ? ? ? ? ? ? .orElseGet(() -> UserProfileResponse.error(USER_NOT_FOUND));
? ? }
請(qǐng)注意,代碼更加簡(jiǎn)潔,對(duì)業(yè)務(wù)邏輯的“干擾”也更少。
這個(gè)例子展示了 monadic 的“控制反轉(zhuǎn)”是多么方便:轉(zhuǎn)換不需要檢查 null,只有當(dāng)值實(shí)際可用時(shí)才會(huì)調(diào)用它們。
“如果/當(dāng)值可用時(shí)做某事”是開始方便地使用 Monads 的關(guān)鍵心態(tài)。
請(qǐng)注意,上面的示例保留了原始 API 的完整內(nèi)容。但是更廣泛地使用該方法并更改 API 是有意義的,因此它們將返回 Optional 而不是 null:
? ? public Optional<UserProfileResponse> getUserProfileHandler4(final User.Id userId) {
? ? ? ? return optionalUserService.findById(userId).flatMap(
? ? ? ? ? ? ? ? user -> userProfileService.findById(userId).map(profile -> UserProfileResponse.of(user, profile)));
? ? }
一些觀察:
代碼更簡(jiǎn)潔,包含幾乎零樣板。
所有類型都是自動(dòng)派生的。雖然并非總是如此,但在絕大多數(shù)情況下,類型是由編譯器派生的---盡管與 Scala 相比,Java 中的類型推斷較弱。
沒有明確的錯(cuò)誤處理,而是我們可以專注于“快樂日子場(chǎng)景”。
所有轉(zhuǎn)換都方便地組合和鏈接,不會(huì)中斷或干擾主要業(yè)務(wù)邏輯。
事實(shí)上,上面的屬性對(duì)于所有的 Monad 都是通用的。
拋還是不拋是個(gè)問題
事情并不總是如我們所愿,我們的應(yīng)用程序生活在現(xiàn)實(shí)世界中,充滿痛苦、錯(cuò)誤和失誤。有時(shí)我們可以和他們一起做點(diǎn)什么,有時(shí)不能。如果我們不能做任何事情,我們至少希望通知調(diào)用者事情并不像我們預(yù)期的那樣進(jìn)行。
在 Java 中,我們傳統(tǒng)上有兩種機(jī)制來通知調(diào)用者問題:
返回特殊值(通常為空)
拋出異常
除了返回 null 我們還可以返回 Option Monad(見上文),但這通常是不夠的,因?yàn)樾枰嚓P(guān)于錯(cuò)誤的詳細(xì)信息。通常在這種情況下我們會(huì)拋出異常。
但是這種方法有一個(gè)問題。事實(shí)上,甚至很少有問題。
異常中斷執(zhí)行流程
異常增加了很多心理開銷
異常引起的心理開銷取決于異常的類型:
檢查異常迫使你要么在這里處理它們,要么在簽名中聲明它們并將麻煩轉(zhuǎn)移到調(diào)用者身上
未經(jīng)檢查的異常會(huì)導(dǎo)致相同級(jí)別的問題,但編譯器不支持
不知道哪個(gè)更差。
Either Monad 來了
讓我們先分析一下這個(gè)問題。我們想要返回的是一些特殊值,它可以是兩種可能的事情之一:結(jié)果值(成功時(shí))或錯(cuò)誤(失敗時(shí))。請(qǐng)注意,這些東西是相互排斥的——如果我們返回值,則不需要攜帶錯(cuò)誤,反之亦然。
以上是對(duì)Either Monad 的幾乎準(zhǔn)確描述:任何給定的實(shí)例都只包含一個(gè)值,并且該值具有兩種可能類型之一。
任何 Monad 的接口都可以這樣描述:
interface Either<L, R> {
? ? <T> Either<T, R> mapLeft(Function<L, T> mapper);
? ? <T> Either<T, R> flatMapLeft(Function<L, Either<T, R>> mapper);
? ? <T> Either<L, T> mapLeft(Function<T, R> mapper);
? ? <T> Either<L, T> flatMapLeft(Function<R, Either<L, T>> mapper);
}
該接口相當(dāng)冗長(zhǎng),因?yàn)樗谧笥抑捣矫媸菍?duì)稱的。對(duì)于更窄的用例,當(dāng)我們需要傳遞成功或錯(cuò)誤時(shí),這意味著我們需要就某種約定達(dá)成一致——哪種類型(第一種或第二種)將保存錯(cuò)誤,哪種將保存值。
在這種情況下,Either 的對(duì)稱性質(zhì)使其更容易出錯(cuò),因?yàn)楹苋菀谉o意中交換代碼中的錯(cuò)誤和成功值。
雖然這個(gè)問題很可能會(huì)被編譯器捕獲,但最好為這個(gè)特定用例量身定制。如果我們修復(fù)其中一種類型,就可以做到這一點(diǎn)。顯然,修復(fù)錯(cuò)誤類型更方便,因?yàn)?Java 程序員已經(jīng)習(xí)慣于從單個(gè) Throwable 類型派生所有錯(cuò)誤和異常。
Result Monad — 專門用于錯(cuò)誤處理和傳播的 Either Monad
所以,讓我們假設(shè)所有錯(cuò)誤都實(shí)現(xiàn)相同的接口,我們稱之為失敗?,F(xiàn)在我們可以簡(jiǎn)化和減少接口:
interface Result<T> {
? ? <R> Result<R> map(Function<T, R> mapper);
? ? <R> Result<R> flatMap(Function<T, Result<R>> mapper);
}
Result Monad API 看起來與 Maybe Monad 的 API 非常相似。
使用這個(gè) Monad,我們可以重寫前面的例子:
? ? public Result<UserProfileResponse> getUserProfileHandler(final User.Id userId) {
? ? ? ? return resultUserService.findById(userId).flatMap(user -> resultUserProfileService.findById(userId)
? ? ? ? ? ? ? ? .map(profile -> UserProfileResponse.of(user, profile)));
}
好吧,它與上面的示例基本相同,唯一的變化是 Monad — Result 而不是 Optional。與前面的例子不同,我們有關(guān)于錯(cuò)誤的完整信息,所以我們可以在上層做一些事情。但是,盡管完整的錯(cuò)誤處理代碼仍然很簡(jiǎn)單并且專注于業(yè)務(wù)邏輯。
“承諾是一個(gè)很重要的詞。它要么成就了什么,要么破壞了什么?!?/strong>
我想展示的下一個(gè) Monad 將是 Promise Monad。
必須承認(rèn),對(duì)于 Promise 是否是 monad,我還沒有找到權(quán)威的答案。不同的作者對(duì)此有不同的看法。我純粹是從實(shí)用的角度來看它的:它的外觀和行為與其他 monad 非常相似,所以我認(rèn)為它們是一個(gè) monad。
Promise Monad 代表一個(gè)(可能還不可用的)值。從某種意義上說,它與 Maybe Monad 非常相似。
Promise Monad 可用于表示譬如對(duì)外部服務(wù)或數(shù)據(jù)庫(kù)的請(qǐng)求結(jié)果、文件讀取或?qū)懭氲?。基本上它可以表示任何需?I/O 和時(shí)間來執(zhí)行它的東西。Promise 支持與我們?cè)谄渌?Monad 中觀察到的相同的思維方式——“如果/當(dāng)價(jià)值可用時(shí)做某事”。
請(qǐng)注意,由于無法預(yù)測(cè)操作是否成功,因此讓 Promise 表示的不是 value 本身而是 Result 內(nèi)部帶有 value 是很方便的。
要了解它是如何工作的,讓我們看一下下面的示例:
...
public interface ArticleService {
? ? // Returns list of articles for specified topics posted by specified users
? ? Promise<Collection<Article>> userFeed(final Collection<Topic.Id> topics, final Collection<User.Id> users);
}
...
public interface TopicService {
? ? // Returns list of topics created by user
? ? Promise<Collection<Topic>> topicsByUser(final User.Id userId, final Order order);
}
...
public class UserTopicHandler {
? ? private final ArticleService articleService;
? ? private final TopicService topicService;
? ? public UserTopicHandler(final ArticleService articleService, final TopicService topicService) {
? ? ? ? this.articleService = articleService;
? ? ? ? this.topicService = topicService;
? ? }
? ? public Promise<Collection<Article>> userTopicHandler(final User.Id userId) {
? ? ? ? return topicService.topicsByUser(userId, Order.ANY)
? ? ? ? ? ? ? ? .flatMap(topicsList -> articleService.articlesByUserTopics(userId, topicsList.map(Topic::id)));
? ? }
}
為了提供整個(gè)上下文,我包含了兩個(gè)必要的接口,但實(shí)際上有趣的部分是 userTopicHandler() 方法。盡管這種方法的簡(jiǎn)單性令人懷疑:
調(diào)用 TopicService 并檢索由提供的用戶創(chuàng)建的主題列表
成功獲取主題列表后,該方法提取主題 ID,然后調(diào)用 ArticleService,獲取用戶為指定主題創(chuàng)建的文章列表
執(zhí)行端到端的錯(cuò)誤處理
后記
Monads 是非常強(qiáng)大和方便的工具。使用“當(dāng)價(jià)值可用時(shí)做”的思維方式編寫代碼需要一些時(shí)間來習(xí)慣,但是一旦你開始使用它,它將讓你的生活變得更加簡(jiǎn)單。它允許將大量的心理開銷卸載給編譯器,并使許多錯(cuò)誤在編譯時(shí)而不是在運(yùn)行時(shí)變得不可能或可檢測(cè)到。