iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0

昨天的文章提到我們要用 FRP 的風格來實作。今天會重構 Repository,明天來修改 RESTful layer。因為我們有寫 Test Case,所以重構的過程中我們也可以很有信心。(絕對不是因為程式小)。

今天的程式碼有放在ironman2022-D15 這個 branch, 所以會說明一些 key point 不會全帶

https://ithelp.ithome.com.tw/upload/images/20220921/201357010AfzJWKC6n.png

引入 Arrow KT

在 pom.xml 加上 Arrow KT 的 Dependency

    <dependency>
      <groupId>io.arrow-kt</groupId>
      <artifactId>arrow-fx-coroutines</artifactId>
      <version>1.1.2</version>
    </dependency>

重構 package

一開始這個專案很散亂都沒有分 package,這個可以使用 IntellJ 的 Refactor 的功能來輕鬆移動。。把原始檔移到相應的 package 。這個 Refactor 功能十分的好用,移完後相對的 dependency IntelliJ 也會幫忙跟著修改。

對著原始檔按右鍵選擇 要移動到的 package
https://ithelp.ithome.com.tw/upload/images/20220921/20135701GCz9F5umK6.png https://ithelp.ithome.com.tw/upload/images/20220921/20135701dKlZLRZ6kl.png

修改完成,順眼多了

https://ithelp.ithome.com.tw/upload/images/20220921/20135701a0F6cLdrxG.png

修改 FilmRepository

這次重構會把所有的 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

先來最簡單的 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

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。

findAll

    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 全方位解析攻略 : 精通原理及實戰,寫出流暢好維護的程式


上一篇
談 Quarkus 錯誤處理,但是我們要用 FRP 引入 Arrow KT
下一篇
重構 Resource Layer - Fold : 在雙軌的盡頭處理 Error - Day16
系列文
Quarkus, Kotlin, Reactive 雲原生服務開發32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言