【譯】RxJava 中的錯誤處理

臨書發表於2019-03-03

RxJava 中的錯誤處理

Drawing
Drawing

一旦你開始使用 RxJava 函式庫寫程式碼,你會發現一些東西能有很多不同的實現方式,但是有時你很難立即確定哪種方法最好,錯誤處理就是其中之一。

那麼,在 RxJava 中處理錯誤的最佳方法是什麼,又有哪些選擇呢?

在 onError 消費者中處理錯誤

假設你有一個 Observable 可能會產生異常。如何處理呢?第一反應應該是直接在 onError 消費者中處理錯誤。

userProvider.getUsers().subscribe(
  { users -> onGetUsersSuccess(users) },
  { e -> onGetUsersFail(e) } // 停止執行,顯示錯誤資訊等。
)複製程式碼

它類似於我們以前使用的 AsyncTasks,並且看起來很像一個 try-catch 塊。

這兒有一個大問題。 假設在 userProvider.getUsers() Observable 中存在程式設計錯誤,導致 NullPointerException 或類似的異常。如果能夠立刻崩潰的話就好了,我們可以現場檢測出問題並且解決。然而上面的程式碼中我們無法看到崩潰,因為錯誤被 onError 處理了,它只會顯示一個錯誤資訊或者其他結果。

更糟糕的是,測試時不會有任何崩潰。測試會失敗,並伴隨著神祕且意想不到的行為。你不得不花時間除錯,而不是立即在一個直觀/具體的棧中找到原因。

預期的和非預期的異常

首先宣告,解釋下我所謂的預期中的和非預期中的異常。

可預期異常不是說程式碼出 bug,而是指執行環境有問題。比如各種 IO 異常,無網路異常等。你的軟體應該適當的對這些異常產生反應,或者顯示錯誤訊息等。預期的異常類似於第二個有效的返回值,它們是方法簽名的一部分。

非預期的異常大多是程式設計錯誤。它們可以並且將會在開發的時候出現,但是它們永遠不應該發生在生產環境中。至少這是一個目標。但是如果它們確實發生了,通常立即使應用崩潰是一個好主意。這有助於提高問題的關注度然後儘快修復之。

在 Java 中,預期中的異常大多是使用受檢異常(直接從 Exception 類子類化)實現的。而大多數預期之外的異常則是使用從 RuntimeException 類派生的未受檢異常實現的。

執行時崩潰異常

所以,如果我們想要崩潰,為什麼不檢查異常是否是一個 RuntimeException,並在 onError 消費者內重新丟擲它呢?如果不僅僅像之前的例子那樣處理它呢?

userProvider.getUsers().subscribe(
  { users -> onGetUsersSuccess(users) },
  { e ->
    if (e is RuntimeException) {
      throw e
    } else {
      onGetUsersFail(e)
    }
  }
)複製程式碼

這可能看起來不錯,但它有一些缺陷:

  1. 在 RxJava 2 中,非常令人費解的是它會在實時執行的應用中崩潰,而在測試中不會。在 RxJava 1 中,則無論實時執行還是測試都會崩潰。
  2. 我們想要崩潰的,除了 RuntimeException 之外還有更多未受檢異常,這包括 Error 等。很難追蹤所有的這類異常。

但主要缺點是這樣的:

在應用開發過程中,你的 Rx 鏈將會變得越來越複雜。你的 Observable 也將會在不同的地方被重用,包括你從沒料到會使用到的上下文中。

假設你已經決定在這個鏈中使用 userProvider.getUsers() 這個 Observable:

Observable.concat(userProvider.getUsers(), userProvider.getUsers())
  .onErrorResumeNext(just(emptyList()))
  .subscribe { println(it) }複製程式碼

當兩個 userProvider.getUsers() 都觸發一個錯誤將會發生什麼?

現在,你可能認為這兩個錯誤都分別對映到一個空列表上,因此將會有兩個空列表被觸發。不過你可能會驚訝的發現,實際上只有一個列表被觸發。這是因為第一個 userProvider.getUsers() 中發生的錯誤將會終止整個鏈的上游, concat 的第二個引數永遠不會被執行。

你看,RxJava 中的錯誤是非常具有破壞性的。它們被設計成致命的訊號來終止整條鏈的上游。它們不應該是你的 Observable 介面的一部分。它們表現為意料之外的錯誤。

Observable 被設計成使用有效輸出來表示錯誤的觸發,這限制了它的使用範圍。複雜的鏈在錯誤的情況下如何工作很不明朗,所以很容易誤用這種 Observable 。這最終會導致錯誤。非常噁心的錯誤,只能偶爾重現的(特殊情況下,比如缺少網路)而且不會留下堆疊痕跡的錯誤。

結果類

那麼,如何設計 Observable 來讓其返回預期的錯誤呢?只需讓它們返回一些 Result 類,即包含操作的結果也包含異常,就像這樣:

data class Result<out T>(
  val data: T?,
  val error: Throwable?
)複製程式碼

將所有預期的異常包含進去,然後將所有不可預期的都放行而使程式崩潰。避免使用 onError 消費者,讓 RxJava 為你控制崩潰。

現在,雖然這種途徑看起來不是特別優雅或直觀,並且產生了相當多的樣板,但是我發現它會導致最少的問題。此外,它看起來像是在 RxJava 中進行錯誤處理的『官方』方式。我看到過它在網際網路的多個討論中被 RxJava 的維護者所推薦。

一些有用的程式碼段

為了使你的 Retrofit Observable 返回 Result 類,你可以使用這個方便的擴充套件功能:

fun <T> Observable<T>.retrofitResponseToResult(): Observable<Result<T>> {
  return this.map { it.asResult() }
    .onErrorReturn {
      if (it is HttpException || it is IOException) {
        return@onErrorReturn it.asErrorResult<T>()
      } else {
        throw it
      }
    }
}

fun <T> T.asResult(): Result<T> {
  return Result(data = this, error = null)
}

fun <T> Throwable.asErrorResult(): Result<T> {
  return Result(data = null, error = this)
}複製程式碼

這樣,你的 Observable userProvider.getUsers() 看起來可以像這樣:

class UserProvider {
  fun getUsers(): Observable<Result<List<String>>> {
    return myRetrofitApi.getUsers()
      .retrofitResponseToResult()
  }
}複製程式碼

掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章