iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 7
0
Software Development

Functional Programming in Kotlin系列 第 7

[RxJava] Observable and error handling

上次的解答, fold 在之後還會看到它的,在 functional programming 中是一個常見的 operator:

fun fold(success: (T) -> Unit, fail: (Throwable) -> Unit) {
    when(this) {
        is Success -> success(this.value)
        is Fail -> fail(this.error)
    }
}

在看了 List, Map, Try 之後,是不是開始抓到了一些感覺了呢?感覺起來...他們好像都有點共通性,都有 map, filter, fold 這些 operator。另外,在之前的章節說過,這些 operator 是一個用來做 functional composition 的強大武器。說到這,應該就有不少人想到,有另一個很熟悉的工具,也有著許許多多不一樣的 opeartor ,他就是擁有著高學習曲線,在 Android 中非常著名的程式庫 - RxJava。

RxJava

想當初我剛學 RxJava 時,是只有物件導向基礎的,那時候學起來非常痛苦,完全無法理解為什麼這樣設計,Observer、Subscriber、Observable,到底什麼是什麼?又有什麼差別?直到兩年後有接觸到 Reactive programming 跟 functional programming 才漸漸的知道該怎麼使用他,以及他們的原則與範式(paradiam),最後,才真正的覺得自己有在寫 Reactive functional programming。

由於篇幅的關係,本系列文章將不會介紹 Reactive programming 的部分,只會針對 functional programming 來講解。但我相信只要了解了 functional programming 對 RxJava 的意義的話,就能更加了解 RxJava 的設計,以及背後想解決的問題,在使用起來會更加得心應手!

說到 RxJava 的核心類別,一定非 Observable 莫屬。那 Observable 的基本用法是什麼呢?

val oneElementObservable = Observable.just(1)
val multiElementObservable = Observable.fromArray(1, 2, 3)

其實跟 List 非常像,對吧?只要把 Observable 的 factory method 轉成 listOf() 的樣式,不就是幾乎一模一樣了嗎?

val oneElementObservable = observableOf(1)
val multiElementObservable = observableOf(1, 2, 3)
val oneElementList = listOf(1)
val multiElementList = listOf(1, 2, 3)

fun <T> observableOf(value: T): Observable<T> {
    return Observable.just(value)
}

fun <T> observableOf(vararg values: T): Observable<T> {
    return Observable.fromArray(*values)
}

而這對 functional programming 來說有什麼意義?我們可以想像成 Observable 跟 List 其實都是一個“容器”,只是容器的運作方式不同罷了,更重要的是,“容器”跟“容器”之間是可以搬移內容物的,下面以程式碼說明,這邊先稍微帶過即可,之後的文章中會再詳細說明。

val observable: Observable<Int> = observableOf(1, 2, 3)
val observableToList: List<Int> = observable.blockingIterable().toList()

val list: List<Int> = listOf(1, 2, 3)
val listToObservable: Observable<Int> = Observable.fromIterable(list)

接下來說到 Observable 另一個重要的元素:Error。

val normalObservable: Observable<Int> = Observable.just(3)
val errorObservable: Observable<Int> = Observable.error(RuntimeException())

等等...這有點眼熟對吧?上一篇才看過類似的程式碼不是嗎?

fun average(sum: Int, count: Int): Try<Int> {
    return try {
        Try.Success(sum / count)
    } catch (e: Throwable) {
        // divide by zero
        Try.Fail(e)
    }
}
// Try.Success 對應到 Observable.just
// Try.Fail 對應到 Observable.error

看來 Observable 也做到了 Try 要做的事情,不是嗎?那麼, Try 也是容器嗎?看起來不太像,對吧?但是換個角度想想,Try 同時包含了兩種可能的狀態,不是“成功”就是“失敗”,不就也算是一種容器嗎?而且,還有一個非常雷同之處:

val observable: Observable<Int> = Observable.just(3)
observable.subscribe( {number: Int ->
    println(number)
}, {error: Throwable ->
    println(error.message)
})

val tr: Try<Int> = Try.Success(1)
tr.fold( {number: Int ->
    println(number)
}, {error: Throwable ->
    println(error.message)
})

Observable 的 subscribe 跟 Try 的 fold 竟然這麼相似!

Error handling

Observable 跟 Try 還有另外一個非常重要的特性:只要發生錯誤,接下來的 operator 都沒有任何作用!( 當然,onError 系列除外)看看下面的例子:

class AccountRepo() {
  // 從 id 找不到 Account 的話會丟 Exception
  fun queryFromId(id: String): Account {...}
  // account 必須還在,有可能被其他人不小心刪掉,如果沒有相對應的 account 一樣會丟 Exception 
  fun updateAccount(account: Account): Boolean {...}
}

class BankService() {
  // 帳戶的錢必需還要夠多,不夠的話會丟 Exception
  fun withDraw(account: Account, amount: Int): Account {...}
}

fun withDrawMoney(accountId: String, amount: Int) {
  Observable.just(accountId)
      .map { id: String -> accountRepo.queryFromId(id) }
      .map { account: Account -> bankService.withDraw(account, amount) }
      .map { account: Account -> accountRepo.updateAccount(account) }
      .subscribe(...)
}

在 withDrawMoney 這個 function 中,有主要三個步驟:尋找帳戶、提款、更新帳戶。其中的任何一個步驟都有可能因為發生一些“意外”而無法跑完整個流程(注意到我用“意外”這個詞了嗎),有可能是找不到用戶,也有可能是錢不夠。當今天一個“意外”發生了,而且發生在第一個步驟,這時會丟出一個 Exception 。然後第二步驟跟第三步驟呢?既然連帳號都找不到了,就不需要被執行了吧!事實上 Observable 就是如此運行的,只要有任何一個地方發生錯誤的話,之後的 operator 將會直接忽略過,然後交給 subscribe 來處理。(當然 onError 系列除外: onErrorReturn, onErrorResumeNext, retry 等等)。而這樣的錯誤處理有什麼好處呢?

  1. 程式碼更容易的被組合了,相較於用 try catch 包起來, onErrorReturn 可以更加容易的各別處理錯誤。
  2. 專注在流程上,不是細節。有時後為了要讓程式碼更安全,更妥善的被處理,會加上非常多的“雜訊”,例如 return, try-catch-finally, ?.let{}, :? 等等

下面示範了其中一種可行的做法,如果找不到帳號,就幫他建立一個新的。

Observable.just(accountId)
            // 將每一個步驟拆解成函式,就可以依據需求更自由的組合、操作 
            .map { id: String -> accountRepo.queryFromId(id) }
            //  onErrorReturn 可以直接處理現在碰到的 Error,回傳新的狀態,然後繼續進行下一個步驟
            .onErrorReturn { Account(...) }
            .map { account: Account -> bankService.withDraw(account, amount) }
            .map { account: Account -> accountRepo.updateAccount(account) }

各位還記得 side effect 嗎?Exception 在上一個章節被當成了一個 Side effect,所以用 Try 這個“容器”來將他包裝起來 。在這裡,Observable 也用了類似的概念,更棒的是,RxJava 提供了各式各樣的 operator 來讓我們來搭配使用:像是 map, filter, reduce, onErrorReturn 等等,不用辛苦的自己手動實作。

小結

Observable 是一個“容器”,其“容器”的內容有可能有零到多個元素,也有可能包含一個 Error。“容器”這個概念呢,在 functional programming 非常常見,之後還會介紹更多。同時,在 functional programming 中可以善用這些 function 小區塊,利用各種不同順序的組合函式,來更妥善的處理各種 Error ,讓我們更專注在流程而不是細節。然而, Observable 還有一項主打:非同步。這時問題就來了:那非同步也是“容器”的一種樣貌嗎?以往我們所熟悉的 Future、Promise 又可以怎樣連結到 Observable 呢?這裡留給讀者一點想像空間,別急,functional programming 的世界才剛始而已呢!


上一篇
Non-deterministic, side effect and Try
下一篇
[RxJava] side effect operators and advanced operator
系列文
Functional Programming in Kotlin30

尚未有邦友留言

立即登入留言