2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2020-05-18

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

今回はKotlin 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()が呼ばれるとします。

__2020/5/19 修正__ @sdkei さんにご指摘いただきました。ありがとうございます。
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

以下本文

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 とは

  2. @sdkei さんによるコメント 及び GitHub kotlinx.coroutines

2
1
7

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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?