昨天的文章提到我們要用 FRP 的風格來實作。今天會重構 Repository,明天來修改 RESTful layer。因為我們有寫 Test Case,所以重構的過程中我們也可以很有信心。(絕對不是因為程式小)。
今天的程式碼有放在ironman2022-D15 這個 branch, 所以會說明一些 key point 不會全帶
在 pom.xml 加上 Arrow KT 的 Dependency
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-fx-coroutines</artifactId>
<version>1.1.2</version>
</dependency>
一開始這個專案很散亂都沒有分 package,這個可以使用 IntellJ 的 Refactor 的功能來輕鬆移動。。把原始檔移到相應的 package 。這個 Refactor 功能十分的好用,移完後相對的 dependency IntelliJ 也會幫忙跟著修改。
對著原始檔按右鍵選擇 | 要移動到的 package |
---|---|
修改完成,順眼多了
這次重構會把所有的 awaitSuspending 在 repo 層處理。 CRUD 我們預期的 transformation 會是以 Either 的盒子封裝結果,Left 是 AppError 這個 sealed class
suspend fun findByEpisodeId (Int) -> Either<AppError, FilmEntity>
suspend fun findAll () -> Either<AppError, List<FilmEntity>>
suspend fun persistOrUpdate (FilmEntity) -> Either<AppError, FilmEntity>
suspend fun delete (FilmEntity) -> Either<AppError, FilmEntity>
suspend fun count () -> Either<AppError, Long>
先來最簡單的 count, 實作上去挖 Quarkus 的原始碼,找出了 KotlinReactiveMongoOperations.Companion.INSTANCE 這個好用的東東,就不用繼承 Base。
import ....KotlinReactiveMongoOperations.Companion.INSTANCE
suspend fun count(): Either<AppError, Long> = Either.catch {
INSTANCE.count(FilmEntity::class.java).awaitSuspending() //(1)
}.mapLeft { AppError.DatabaseProblem(it) } //(2)
動作說明
(1) 用 Either.catch 包住資料庫的存取的動作。如果正常取代就會是 Either.Right<Long>
,有 Exception 就是 Either.Left<Throwable>
。
(2) mapLeft { AppError.DatabaseProblem(it) }
這動作是說如果有左值,會把throwable 轉成 AppError, 方便之後的 chain.
findById 又進階一點了,會用到 map 與 flapMap
suspend fun findByEpisodeId(id: Int): Either<AppError, FilmEntity> = Either.catch {
INSTANCE.find(FilmEntity::class.java, "episodeId", id)
.firstResult().awaitSuspending()
}.mapLeft { AppError.DatabaseProblem(it) }
.flatMap { // (2)
it.toOption().toEither(ifEmpty = { AppError.NoThisFilm(id) }) // (3)
}.map { it as FilmEntity } // (1)
(1) 對 Either 作 map 表示要對 Right 值 apply 後面那個 function , 這裡只是一個 casting
(2) 對 Eiter 作 flatmap 表示要對 Right 值 apply 後面那個 function,並且有錯誤的話會把雙層 Either 轉成單個 Either. 關於 map, flatMap 的觀念可以參考 這篇鐵人賽
(3) toOption().toEither
又是什麼操作? 表示要把 T? 轉成 Option<None,Some>,toEither 表示要把 None (None) 視為錯誤轉去 Either.Left。
suspend fun findAll(): Either<AppError, List<FilmEntity>> = Either.catch {
INSTANCE.findAll(FilmEntity::class.java).list().awaitSuspending()
}.mapLeft { e -> AppError.DatabaseProblem(e) }
.map { it.map { obj -> obj as FilmEntity } } // (5)
(5) 怪怪,這裡怎麼 map 再 map , 第一個 map 是 Either 的 map。
第二個 map 是因為取出來是一個 list, 要對裡面的所有元素作操作,所以用的是 Collection 的 map 表示要對集合的所有元素 apply 一個function, 且還你一個新的集合 (這個也符合 FP 裡 immuntable 的操作)
更多 Collection 的用法可參考 JetBrains 技術傳教士 - 聖佑的大作 Kotlin Collection 全方位解析攻略 : 精通原理及實戰,寫出流暢好維護的程式