型で保証できるものとできないもの
普段私は Kotlin の型システムを活用して、コンパイル時に不正な状態を表現できない設計を心がけています。
例えば、
sealed classで状態を表現するvalue classで ID をラップするnullを型で排除する
といった方法です。
しかし、あるとき「存在する PaymentId しか関数に渡せないようにできないか?」と考えました。
結論から言うと、これは型だけでは保証できません。
なぜなら、
fun observePayment(id: PaymentId)
という関数に渡された ID が実際にデータベースに存在するかどうかは、コンパイル時には分からないからです。
型で保証できるのは、
- null ではない
- 状態の種類
- 値の構造
- 不変条件
など、コンパイル時に判定できる性質です。
一方で、
- DB にレコードが存在する
- ファイルが存在する
- API が応答する
- 他ユーザーによって削除されていない
といったものは実行時の状態に依存するため、型だけでは保証できません。
当たり前と言えば当たり前なのですが、今回再確認したことは
型で保証できるのは「コンパイル時に確定できること」
実行時の状態に依存することは、実行時チェックで保証する
ということでした。
例えば「一覧画面から選択された ID なので存在するはず」という前提がある場合でも、その後、別の画面からそのデータが削除されている可能性もあるため、 ID が存在していない可能性もあります。
その場合は、 Repository や UseCase で requireNotNull() を使ったり、独自例外を投げたりして不整合を検知することになります。
型安全な設計を考える際は、「これはコンパイル時に分かる情報か?それとも実行時にしか分からない情報か?」を意識すると、どこまでを型で表現すべきか判断しやすくなります。