Kotlin Coroutinesと関数オブジェクトを用いた非同期処理コードの効果的な書き方の提案

こんにちは。いつ と申します。
普段はMiRm Development Teamの一員として、MiRmというゲームサーバーホスティングサービスの開発をしております。

今回は[Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines)を用いた非同期処理のコードについて、より効果的な書き方を提案したいと思います。

# どういう処理をするのか
例として「Android上において、RestfulなWebAPIから取得した情報の内容によってFragmentに表示する内容を変える」というシチュエーションを考えていくことにします。
非メインスレッドから描画処理を行うと当然ながらエラーが発生しますし、逆にメインスレッドでネットワークアクセスをしてもエラーが発生します。特にAndroid上での非同期処理の実装は悩みどころです。

# 使用するクラスの説明
* __WebAPI__ WebAPIにアクセスし、getやpostを行うクラス。
* __ResponseModel__ WebAPIから取得したレスポンスのオブジェクト。

各クラスの詳細な処理は割愛します。
## ResponseModel.kt
```ResponseModel.kt
data class ResponseModel(
    val statusCode: Int,
    val data1: String
) {
    companion object {
        const val STATUS_SUCCESS = 0
        const val STATUS_ERROR = 1
        const val STATUS_DATABASE_ERROR = 2
        const val STATUS_NOT_FOUND = 3
    }
}
```

## WebAPI.kt
```WebAPI.kt
object WebAPI {
    fun getUserData(url: String): ResponseModel {
        // get処理
        val model = ResponseModel(ResponseMode.STATUS_SUCCESS, url)
        return model
    }
}
```

以上二つのクラスをベースに考えていきます。

# 何も考えずに書くと...
以下、何も考えずに非同期処理を実行する処理を書いてみます。ボタンを押すとonButtonClick()が呼ばれるとします。

<details><summary>__2020/5/19 修正__ @sdkei さんにご指摘いただきました。ありがとうございます。</summary><div>

```Presenter.kt
GlobalScope.async(Dispatchers.Default) {
    WebAPI.getUserData("...")
}.await().let{ /* 処理 */ }

// から

withContext(Dispatchers.IO) {
    WebAPI.getUserData("...")
}.let { /* 処理 */ }

/* 
に修正しました。
・async/await -> withContext
・Dispatchers.Default -> Dispatchers.IO
*/
```
【なぜ修正したのか】

* ``` GlobalScope.async(...) {}.await().let{}``` だとcoroutineを新たに生成するため、リソースを浪費してしまうから。[^1]
* Dispatchers.DefaultはCPUのコア数ぶんしかないため、スレッドが足りなくなるから。[^2]

以下本文

</div></details>


```Presenter.kt
fun onButtonClick() = GlobalScope.launch(Dispatchers.Main) {
    showToast("Loading...")

    withContext(Dispatchers.IO) {
        WebAPI.getUserData("https://...")
    }.let {
        when (it.statusCode) {
            ResponseModel.STATUS_SUCCESS -> {
                setText("Success!!")
                showDialog()
            }
            ResponseModel.STATUS_ERROR -> setText("Error!!")
            ResponseModel.STATUS_DATABASE_ERROR -> setText("Database Error!!")
            ResponseModel.STATUS_NOT_FOUND -> setText("Not Found!!")
        }
    }

    showToast("Process Finished!!")
}
```

## どういった問題が起こるか
* インデントが多く、ブロックが何重にもネストしていて非常に見にくい
* statusCodeの種類が増えると、その分条件分岐が増えて拡張が困難
* WebAPIごとに共通な処理が多いため、コードが冗長になる
* ビジネスロジックを不用意に荒らすことになりかねない

などといった問題が発生し、バージョンアップ時の拡張性が損なわれたり、バグが見つかりにくくなるなどコードの保守にも影響する可能性があります。

# 提案する書き方
上記の問題を解消するために、関数オブジェクトとinvokeを用いたコードの書き方を提案します。
提案する書き方だと、クラスの量は少し増えますがコードの共通化も図れ、非常に見やすいものとなります。

## BaseManager
ベースとなる非同期マネージャクラスです。エラー処理などを共通化します。

```BaseManager.kt
package dev.itsu.test

open class BaseManager<T: BaseManager<T>> {

    protected var onInitialize: () -> Unit = {}
    protected var onFinish: () -> Unit = {}
    protected var onError: () -> Unit = {}

    // 処理開始前に呼ばれる関数
    fun onInitialize(func: () -> Unit): T {
        this.onInitialize = func
        return this as T
    }

    // 処理終了時に呼ばれる関数
    fun onFinish(func: () -> Unit): T {
        this.onFinish = func
        return this as T
    }

    // エラー時に呼ばれる関数
    fun onError(func: () -> Unit): T {
        this.onError = func
        return this as T
    }

}
```

## GetUserDataManager
実際の非同期処理の実装クラスです。

```GetUserDataManager.kt
package dev.itsu.test

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch

class GetUserDataManager: BaseManager<GetUserDataManager>() {

    private var onSuccess: (model: ResponseManager) -> Unit = {}
    private var onDatabaseError: () -> Unit = {}
    private var onNotFound: () -> Unit = {}

    fun getUserData(url: String) = GlobalScope.launch(Dispatchers.Main) {
        onInitialize.invoke()

        withContext(Dispatchers.IO) {
            WebAPI.getUserData(url)

        }.let {
            when (it.statusCode) {
                ResponseModel.STATUS_SUCCESS -> onSuccess(it)
                ResponseModel.STATUS_ERROR -> onError()
                ResponseModel.STATUS_DATABASE_ERROR -> onDatabaseError()
                ResponseModel.STATUS_NOT_FOUND -> onNotFound()
            }
            onFinish.invoke()
        }
    }

    fun onSuccess(func: (model: ResponseModel) -> Unit): GetUserDataManager {
        this.onSuccess = func
        return this
    }

    fun onDatabaseError(func: () -> Unit): GetUserDataManager {
        this.onDatabaseError = func
        return this
    }

    fun onNotFound(func: () -> Unit): GetUserDataManager {
        this.onNotFound = func
        return this
    }
}
```

## Presenter（非同期処理実行クラス）
実際にこのクラスで非同期処理を実行します。

```Presenter.kt
fun onButtonClick() {
    GetUserDataManager()
        .onInitialize { showToast("Loading...") }
        .onFinish { showToast("Process Finished!!") }
        .onError { setText("Error!") }
        .onDatabaseError { setText ("Database Error") }
        .onNotFound { setText ("Not Found.") }
        .onSuccess { 
            setText("Your name is ${it.data1}") 
            showDialog()
        }
        .getUserData("https://...")
}
```

## 結果
上記のPresenterクラスのように、条件分岐は無しに、ネストも最小限になって非常にコードが見やすくなりました。また、BaseManagerクラスにてonErrorなどのWebAPIにおける共通の処理を書くことによって、GetUserDataManagerをはじめとするWebAPIのマネージャクラスにおける実装も最小限のものとなりました。


いかがだったでしょうか。
このように実装次第で、ビジネスロジック部分の処理をより簡潔に、見やすく書くことができるようになります。
自分もまだまだkotlin初心者ですが、実装を工夫して開発に臨んでいきたいと思います。

最後まで読んでいただき、ありがとうございました。


[^1]: @sdkei さんによるコメント 及び [Kotlin coroutine withContext とは](https://qiita.com/favolabo/items/1e8ae7f32bc0917189fb)
[^2]: @sdkei さんによるコメント 及び [GitHub kotlinx.coroutines](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html)