- 原文地址:Error handling in RxJava
- 原文作者:Dmitry Ryadnenko
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:星辰
- 校對者:張拭心 Liz
RxJava 中的錯誤處理
一旦你開始使用 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)
}
}
)複製程式碼
這可能看起來不錯,但它有一些缺陷:
- 在 RxJava 2 中,非常令人費解的是它會在實時執行的應用中崩潰,而在測試中不會。在 RxJava 1 中,則無論實時執行還是測試都會崩潰。
- 我們想要崩潰的,除了
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()
}
}複製程式碼
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。