こんにちは。コミュニケーションアプリ「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