5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlin Multiplatformなプロジェクトで便利だった小技いくつか

Posted at

はじめに

ここ半年ほどKotlin Multiplatformなコードを書く中で、使って便利だった小技がいくつかあったため共有します。

ログ出力時にそのログの出力元のクラス名を利用する

現在のプロジェクトではロガーについてシンプルなインタフェースを作り、実装をDIで注入するという方式を取っています。

ログの各行にはそれを出力したクラス名くらいは載っていてほしいものですが、ログ出力のたびに出力関数の引数に this::class.simpleName のようなボイラープレートを渡すのも少々面倒です。

そのため、以下のようなちょっとした工夫をおこない、ログ出力コードの簡便さとログの情報量を両立させています。

まず、ロガーのインタフェースのなかに以下の Attatched のような、ロガーをプロパティとして持っていることを表すインタフェースを入れ子で用意しておきます。

internal interface Logger {
    enum class Severity {
        TRACE, DEBUG, INFO, WARN, ERROR, FATAL
    }
    fun log(severity: Severity, content: () -> String)

    interface Attached {
        val logger: Logger
    }
}

また、適当な場所に以下のようなユーティリティを生やしておきます。

private inline fun log(
    logger: Logger,
    severity: Logger.Severity,
    className: String?,
    crossinline content: () -> String
) {
    logger.log(severity){"<${className?:"unknown"}>: ${content()}"}
}

internal fun Logger.Attached.putTrace(content: () -> String) {
    log(logger, Logger.Severity.TRACE, this::class.simpleName, content)
}
internal fun Logger.Attached.putDebug(content: () -> String) {
    log(logger, Logger.Severity.DEBUG, this::class.simpleName, content)
}
// :
// :

こうすることで、以下のように、Logger.Attatched を実装したクラスの中では、 putDebug {(出力する内容)} のような記述をするだけで、ログの内容にクラス名を自動的に含められるようになります。

internal class Klass(di: DI): Logger.Attatched {
    override val logger: Logger by di.instance()

    fun f() {
        putDebug { "Hello World!" }
    }
}

Kotlin/JVMのリフレクションなどならもう少しスマートな方法があるかもしれませんが、とりあえずこのような小手先のテクニックでも十分役に立っています。

気になる点としては、logger がクラス外に漏れてしまっている点が挙げられます。 internal なクラスで使う分には、そのクラスの外側であえてそのクラスの文脈であることを明示したいときに 、Klass().logger.putDebug{} のように使えて便利な場面もありますが、ライブラリ等の一部としてモジュール外に公開するクラスには使用しない方がいいかもしれません。

Result 周りを少し書きやすくする

kotlinx.coroutineではコルーチン間を跨いだ例外を扱うのが面倒な場合が多いため、自然と(Rustなどでよくやるように)例外も関数の返値として陽に扱うケースが出てくるかと思います。その際に、正常系の値や例外をラップする型として、Kotlin標準のResult1 や ArrowのEitherを選択する場合が多いかと思います。

現在のプロジェクトでは、一部を除いて Result を使用することを決めたため、値や例外を Result に変換するために、Result.success(v)Result.failure(e) というコードが頻出するようになりました。

これは長くて冗長であるため、適当な場所に

@Suppress("NOTHING_TO_INLINE")
inline fun <T> T.success(): Result<T> = Result.success(this)
@Suppress("NOTHING_TO_INLINE")
inline fun <T, F> F.failure(): Result<T> where F: Throwable = Result.failure(this)

のようなユーティリティ関数を書いておき、v.success()e.failure() と書けるようにしました。

今のところこの程度の機能で十分ではありますが、 Result をよりガッツリ扱いたい場合はkotlin-resultなどのライブラリを使ってみるのも手かもしれません。

Deffered な値にお手軽にログを仕込む

kotlin.coroutineを使った非同期処理を行う場合、async などの返値としてDeferred 型の値を扱うことがあるかと思います。

またデバッグのために、こうした値のキャンセル時や例外送出時にログを吐かせたいケースが多々あるかと思います2

そのような場合、下記のようなコードを適当な場所に置いておき、

internal fun <T> Deferred<T>.attachTracer(
    onComplete: () -> Unit,
    onCancelled: () -> Unit,
    onThrown: (Throwable) -> Unit
): Deferred<T> = apply {
    invokeOnCompletion {
        when (it) {
            null -> onComplete()
            is CancellationException -> onCancelled()
            else -> onThrown(it)
        }
    }
}
val v = async { f() }.attachTracer(
    onComplete = { putTrace { "f() completed" } },
    onThrown = { putError { "error on f(): $it" } },
    onCancelled = { putTrace { "f() cancelled" } }
)

上記コードのように attachTracer をつけ足してやると、コードをあまり変更せずに簡単にログを仕込めて便利です。

  1. ちょっと前までは関数の返値に使えない不便な型で、自力で似たようなものを実装したりしましたが、今では他の言語の ResultEither と同じような感覚で扱えるようになりました。

  2. 特にKMPではデバッガにあまり恵まれないため

5
0
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
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?