LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック: 第 2 回(確認したかどうか確認した?)

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。

この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 2 回です。Weekly Report については、第 1 回の記事を参照してください。

確認したかどうか確認した?

以下の関数はプログレスバーを表示するための関数で、プログレスは 0 から 1 までの値をとります。

/**
 * Shows a progress bar blocking other UI interaction with a given progress
 * ratio in \[0, 1\].
 */
fun showProgressBar(progress: Float)

ここで、プログレスバーの取りうる値が 0 から 1 であることを保証するために、以下のように実装しました。この中でも caller は、showProgressBar を使う側のコードです。また、coerceAtMost  coerceAtLeast は、それぞれ数値を上限と下限以内に収めるための関数です (それぞれ、min  max と同等です)。

fun caller() {
    ... // snip
    val cappedProgress = progress.coerceAtMost(1F)
    showProgressBar(cappedProgress)
}

fun showProgressBar(progress: Float) {
    val progressInRange = progress.coerceAtLeast(0F)
    ... // snip
}

このコードの問題点は何でしょうか?

誰かがチェックするだろうからヨシッ

このコードには、[0, 1] の範囲を確認する責任の所在が明確でないという問題があります。coerceAtMost による上限の指定は呼び出し元で行われている一方で、coerceAtLeast での下限の指定は呼び出し先で行われています。このような関数は使い方を間違いやすいだけでなく、仕様・実装の変更の際にバグを埋め込みやすくなります。

基本的な考え方として、関数(やメソッド)の呼び出しは「フールプルーフ(間違いようがない)」であるべきです。それを実現するための方法を 2 つほど紹介します。

Option 1: 信頼できるは己のみ

1 つ目は 関数の呼び出し先 の中で確認を行う方法です。これは特に、確認済・未確認という状態を型安全に取り扱えない状況で特に有効です。

通常の Float は変域が [0, 1] に限定されていないため、実引数として与えられる Float が常に [0, 1] の範囲に収まっていることを保証することは難しいでしょう。その実引数はユーザが直接入力した値かもしれませんし、外部システムから与えられた値かもしれません。多くの場合、実引数は信頼できないと考えるべきです。

以下の実装では、[0, 1] の範囲外の数値が与えられた場合は、それぞれ 0, 1 として取り扱っています。

fun showProgressBar(progress: Float) {
    val progressInRange = progress.coerceIn(0F, 1F)
    ... // snip
}

呼び出し元によって、範囲外の値に対するエラーハンドリングを変えたい場合は、「正しく処理できたかどうか」を戻り値で知らせる方法もあります。以下のコードでは、範囲外の実引数が与えられた場合に false を返しています。

fun showProgressBar(progress: Float): Boolean {
    if (progress !in 0F..1F) {
        return false
    }

    ... // snip
    return true
}

ここで、エラーが発生し得ることを呼び出し元に意識させたい場合は、呼び出し元に戻り値の処理を強制させる方法 (Java の Error Prone ライブラリの @CheckReturnValue や Android Kotlin の @CheckResult、Swift や Rust の通常の関数) や、 検査例外を使う方法などがあります。

Option 2: 型安全な「検査済証」

2 つ目の方法は、値が一定の範囲に入っていることを保証する型を作り、showProgressBar には正しい値しか渡されないようにすることです。

showProgressBar の例ならば、以下のような「検査済」を示すモデルを作るとよいでしょう。

class ProgressRatio(rawValue: Float) {
    val value = rawValue.coerceIn(0F..1F)
}

fun showProgressBar(progressRatio: ProgressRatio)

呼び出し元で範囲外の値に対するエラーハンドリングを行いたい場合は、ファクトリ関数 (もしくは failable initializer) を作るとうまくいくことが多いです。

class ProgressRatio private constructor(val value: Float) {
    companion object {
        fun of(value: Float): ProgressRatio? =
            if (value in 0F..1F) ProgressRatio(value) else null
    }
}

このとき、ファクトリ関数でエラーを示す値は、型安全な値 (「安全な」 null  Optional 等) を使うと頑健なコードになります。非検査例外 (IllegalArgumentException 等) を使うと、詳細を知らないと使えないクラスになってしまいます。


一言まとめ: 「チェック済みである」ことを暗黙的に前提にしたコードは、できる限り避けたほうが良い。

キーワード: implicit dependency, state check logic, type safety