2
2

【Kotlin】runCatching の後をメソッドチェーンにしてコードを読みやすくする

Last updated at Posted at 2024-07-06

はじめに

こんなコードを見たことはないだろうか。

MainViewModel.kt
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行だからいいものの、複数行でかつ ifwhen 等 があるとコードを追うのがかなり辛くなる。経験上、こういうコードにはかなり高い確率で既に不具合が存在しており、そして将来的に不具合が埋め込まれる。

では、ネストを解消した場合はどうだろう。

MainViewModel.kt
    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 等のスコープ関数を使ってメソッドチェーンにしてみる。

MainViewModel.kt
    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 するたびに成功・失敗の判定するのが面倒である。

今記事では mapCatchingrecoverCatching を使ってメソッドチェーンを実現し、もっと読みやすいコードにしてみる。
mapCatchingrecoverCatchingを初めて見るという人は予め予習をしなくてもサンプルコードを見てもらえればどういう関数であるかわかるはずなのでご心配なく。

mapCatching で処理を チェーン させる

一番シンプルなパターン。 2回目以降の runCatchingmapCatching に置き換えて処理をチェーンする。

MainViewModel.kt
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

mapCatchingException が発生した場合は、 onFailure に入る

mapCatching で処理が失敗、つまり Exception が発生した場合に何か処理を実行したい場合は onFailure で入る。

MainViewModel.kt
    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

mapCatchingException が発生しない場合は onFailure に入らないのでご心配なく。

MainViewModel.kt
    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

mapCatchingException が発生した場合は、 以降の mapCatching をスキップして onFailure に入る。この動作は嬉しい。一連の処理の中で失敗処理を1度だけ実行したい場合は一番下に onFailure を配置すれば良い。

MainViewModel.kt
    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 に入るのでご心配なく。

MainViewModel.kt
    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 を使おう。

MainViewModel.kt
    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 で失敗を成功に戻す

mapCatchingException が発生した場合でもチェーンを続行したい場合は recoverCatching を使う。 recoverCatching が正常であればチェーンを続行できる。 recovery という名の通り、失敗した場合の正常に戻す処理を書くと良いだろう。

MainViewModel.kt
    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 は引数としてThowable を受け取るので、 これまでのチェーンで繋いで来た値がここで途切れてしまうことだ。

mapCatchingException が発生しない場合は、 recoverCatching に入らない。

MainViewModel.kt
    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 に入る。

MainViewModel.kt
    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 に入る。

MainViewModel.kt
    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 を早めに配置する

mapCatchingrecoverCatching によるメソッドチェーンの途中でチェーンを断ち切りたい時があるだろう。その場合は、 失敗が発生する場所に onFailure を配置する。

MainViewModel.kt
    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 でメソッドチェーンができるのでは? という淡い期待を断ち切っておく。

MainViewModel.kt
    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行程度に収めようとするマインドが働くはずで、そこが一番のメリットだと思っている。

MainViewModel.kt
    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()}")
        }
    }
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2