はじめに
こんなコードを見たことはないだろうか。
class MainViewModel : ViewModel() {
val TAG = "RUN-CATCHING-CHAIN"
private fun badCase() {
viewModelScope.launch {
runCatching {
randomThrows()
}.onSuccess {
runCatching {
randomThrows()
}.onSuccess {
val result = runCatching {
randomThrows()
}
Log.d(TAG, "[bad case] result: ${result.getOrNull()}")
}.onFailure {
Log.w(TAG, "onFailure: $it.message")
}
}.onFailure {
Log.w(TAG, "onFailure: $it.message")
}
}
}
private fun randomThrows(): String {
return if ((0..1).random() == 0) {
throw Exception("Exception")
} else {
"R"
}
}
}
見ての通り、 onSuccess
でどんどんネストしていくコードである。 onSuccess
のラムダが1行だからいいものの、複数行でかつ if
や when
等 があるとコードを追うのがかなり辛くなる。経験上、こういうコードにはかなり高い確率で既に不具合が存在しており、そして将来的に不具合が埋め込まれる。
では、ネストを解消した場合はどうだろう。
fun badCase2() {
viewModelScope.launch {
val result1 = runCatching {
randomThrows()
}
if (result1.isFailure) {
Log.w(TAG, "onFailure: ${result1.exceptionOrNull()?.message}")
return@launch
}
val result2 = runCatching {
Log.d(TAG, "result1:${result1.getOrNull()}")
randomThrows()
}
if (result2.isFailure) {
Log.w(TAG, "onFailure: ${result2.exceptionOrNull()?.message}")
return@launch
}
val result3 = runCatching {
Log.d(TAG, "result2:${result2.getOrNull()}")
randomThrows()
}
if (result3.isSuccess) {
Log.d(TAG, "result3:${result2.getOrNull()}")
} else {
Log.w(TAG, "onFailure: ${result3.exceptionOrNull()?.message}")
}
}
Result
の中身をいちいち取り出すのが面倒。また、変数が多くあり、かつスコープが広いので扱う変数を間違ってしまいそうである。
変数の取り扱い間違いを解消するため、let
等のスコープ関数を使ってメソッドチェーンにしてみる。
fun badCase3() {
viewModelScope.launch {
runCatching {
randomThrows()
}.let { result1 ->
if (result1.isFailure) {
Log.w(TAG, "onFailure: ${result1.exceptionOrNull()?.message}")
return@launch
}
runCatching {
Log.d(TAG, "result1:${result1.getOrNull()}")
randomThrows()
}
}.let { result2 ->
if (result2.isFailure) {
Log.w(TAG, "onFailure: ${result2.exceptionOrNull()?.message}")
return@launch
}
runCatching {
Log.d(TAG, "result2:${result2.getOrNull()}")
randomThrows()
}
}.also { result3 ->
if (result3.isSuccess) {
Log.d(TAG, "result3:${result3.getOrNull()}")
} else {
Log.w(TAG, "onFailure: ${result3.exceptionOrNull()?.message}")
}
}
}
}
まだ、 Result
の中身を取り出す面倒が残っており、またこれに限らずこれまでのサンプルコード全部に言えることだが、 runCatching
するたびに成功・失敗の判定するのが面倒である。
今記事では mapCatching
、 recoverCatching
を使ってメソッドチェーンを実現し、もっと読みやすいコードにしてみる。
mapCatching
、 recoverCatching
を初めて見るという人は予め予習をしなくてもサンプルコードを見てもらえればどういう関数であるかわかるはずなのでご心配なく。
mapCatching
で処理を チェーン させる
一番シンプルなパターン。 2回目以降の runCatching
を mapCatching
に置き換えて処理をチェーンする。
class MainViewModel : ViewModel() {
val TAG = "RUN-CATCHING-CHAIN"
fun case1() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
it + "2" // ②
}.mapCatching {
it + "3" // ③
}.mapCatching {
it + "4" // ④
}
Log.d(TAG, "[case1] result:${result.getOrNull()}") // ⑤
}
}
}
// [case1] result:1234
mapCatching
で Exception
が発生した場合は、 onFailure
に入る
mapCatching
で処理が失敗、つまり Exception
が発生した場合に何か処理を実行したい場合は onFailure
で入る。
fun case2() {
viewModelScope.launch {
val result = runCatching {
"1" //①
}.mapCatching {
throw Exception("Exception") //②
}.onFailure {
Log.w(TAG, "onFailure: $it.message") //③
}
Log.d(TAG, "[case2] result: ${result.getOrNull()}") //④
}
}
// [case2] result: null
mapCatching
で Exception
が発生しない場合は onFailure
に入らないのでご心配なく。
fun case3() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
it + "2" // ②
}.onFailure {
Log.w(TAG, "onFailure: $it.message")
}
Log.d(TAG, "[case3] result: ${result.getOrNull()}") // ③
}
}
// [case3] result: 12
mapCatching
で Exception
が発生した場合は、 以降の mapCatching
をスキップして onFailure
に入る。この動作は嬉しい。一連の処理の中で失敗処理を1度だけ実行したい場合は一番下に onFailure
を配置すれば良い。
fun case4() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
//NOTE: 直接 throw Exception() を実行すると次の mapCatching の
// it がコンパイルエラーになるのでメソッド経由で Exception を発生させる
throwException() // ②
}.mapCatching {
it + "3"
}.onFailure {
Log.w(TAG, "onFailure: $it.message") // ③
}
Log.d(TAG, "[case4] result: ${result.getOrNull()}") // ④
}
}
private fun throwException(): String {
throw Exception("Exception")
}
//[case4] result: null
初っ端の runCatching で Exception が発生してしまっても、以降の mapCatching をスキップして onFailure
に入るのでご心配なく。
fun case5() {
viewModelScope.launch {
val result = runCatching {
throwException() // ①
}.mapCatching {
it + "2"
}.onFailure {
Log.w(TAG, "onFailure: $it.message") // ②
}
Log.d(TAG, "result: ${result.getOrNull()}") // ③
}
}
// [case5] result: null
onSuccess
を使うとどうなるの? という疑問が湧くだろうから、 チェーンの途中に onSuccess
を配置した場合を検証してみる。結果、 mapCatching
で正常しても次の onSuccess
には入らない。正常処理をチェーンする場合はやっぱり mapCatching
を使おう。
fun case6() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
it + "2" // ②
}.onSuccess {
it + "3"
}.onFailure {
Log.w(TAG, "onFailure: ${it.message}")
}
Log.d(TAG, "[case6] result: ${result.getOrNull()}") // ③
}
}
// [case6] result: 12
recoverCatching
で失敗を成功に戻す
mapCatching
で Exception
が発生した場合でもチェーンを続行したい場合は recoverCatching
を使う。 recoverCatching
が正常であればチェーンを続行できる。 recovery
という名の通り、失敗した場合の正常に戻す処理を書くと良いだろう。
fun case7() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
throw Exception("Exception") // ②
}.recoverCatching {
"3" // ③
}.mapCatching {
it + "4" // ④
}.onFailure {
Log.w(TAG, "onFailure: $it.message")
}
Log.d(TAG, "[case7] result: ${result.getOrNull()}") // ⑤
}
}
// [case7] result: 34
ただし、残念な点がある。それは、 recoverCatching
は引数としてThowabl
e を受け取るので、 これまでのチェーンで繋いで来た値がここで途切れてしまうことだ。
mapCatching
で Exception
が発生しない場合は、 recoverCatching
に入らない。
fun case8() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
it + "2" // ②
}.recoverCatching {
"3"
}.mapCatching {
it + "4" // ③
}
Log.d(TAG, "[case8] result: ${result.getOrNull()}") // ④
}
}
// [case8] result: 124
recoverCatching
でさらに Exception
が発生した場合は、以降の mapCatching
をスキップして onFailure
に入る。
fun case9() {
viewModelScope.launch {
val result = runCatching {
throw Exception("Exception") // ①
}.recoverCatching {
throwException() // ②
}.mapCatching {
it + "3"
}.onFailure {
Log.w(TAG, "onFailure ${it.message}") // ③
}
Log.d(TAG, "[case9] result: ${result.getOrNull()}") // ④
}
}
// [case9] result: null
recoverCatching
でさらに Exception
が発生した場合、 以降に recoverCatching
が存在すれば recoverCatching
に入る。
fun case10() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
throw Exception("Exception") // ②
}.recoverCatching {
throwException() // ③
}.mapCatching {
it + "4"
}.recoverCatching {
"5" // ④
}
Log.d(TAG, "[case10] result: ${result.getOrNull()}") // ⑤
}
}
// [case10] result: 5
メソッドチェーンを途中で切りたい場合は onFailure
を早めに配置する
mapCatching
や recoverCatching
によるメソッドチェーンの途中でチェーンを断ち切りたい時があるだろう。その場合は、 失敗が発生する場所に onFailure
を配置する。
fun case11() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.mapCatching {
throwException() // ②
}.onFailure {
Log.w(TAG, "onFailure: ${it.message}") // ③
}.mapCatching {
it + "4"
}
Log.d(TAG, "[case11] result: ${result.getOrNull()}") // ④
}
}
// [case11] result: null
onSuccess
では mapCatching
のようなメソッドチェーンができない
最後に mapCatching
を使わなくても onSuccess
でメソッドチェーンができるのでは? という淡い期待を断ち切っておく。
fun case12() {
viewModelScope.launch {
val result = runCatching {
"1" // ①
}.onSuccess {
it + "2" // ②
}.onSuccess {
it + "3"
}
Log.d(TAG, "[case12] result: ${result.getOrNull()}") // ③
}
}
// [case12] result: 1
最後に
以下の点において読みやすいコードになったのではないだろうか。
-
Resull
の中身を取り出す面倒が軽減された - 失敗はスルーできるので
runCatching
するたびに成功、失敗の判定の必要がない -
recoverCatching
によって失敗ルートを成功ルートに戻すことが可能になった。失敗ルートの中に成功ルートを実装する必要が無くなった
私的には宣言的にコードを書くことによって、各メソッドのラムダの中をモジュール化によって1行程度に収めようとするマインドが働くはずで、そこが一番のメリットだと思っている。
fun runChain() {
viewModelScope.launch {
val result = runCatching {
doSomething1()
}.mapCatching {
doSomething2()
}.recoverCatching {
doSomething3()
}.mapCatching {
doSomething4()
}.recoverCatching {
doSomething5()
}.mapCatching {
doSomething6()
}.onFailure {
Log.w(TAG, "onFailure: ${it.message}")
}.mapCatching {
doSomething7()
}.onFailure {
Log.w(TAG, "onFailure: ${it.message}")
}
Log.d(TAG, "[runChain] result: ${result.getOrNull()}")
}
}