型で保証できるものとできないもの

普段私は Kotlin の型システムを活用して、コンパイル時に不正な状態を表現できない設計を心がけています。

例えば、

  • sealed class で状態を表現する
  • value class で ID をラップする
  • null を型で排除する

といった方法です。

しかし、あるとき「存在する PaymentId しか関数に渡せないようにできないか?」と考えました。

結論から言うと、これは型だけでは保証できません。

なぜなら、

fun observePayment(id: PaymentId)

という関数に渡された ID が実際にデータベースに存在するかどうかは、コンパイル時には分からないからです。

型で保証できるのは、

  • null ではない
  • 状態の種類
  • 値の構造
  • 不変条件

など、コンパイル時に判定できる性質です。

一方で、

  • DB にレコードが存在する
  • ファイルが存在する
  • API が応答する
  • 他ユーザーによって削除されていない

といったものは実行時の状態に依存するため、型だけでは保証できません。

当たり前と言えば当たり前なのですが、今回再確認したことは

型で保証できるのは「コンパイル時に確定できること」

実行時の状態に依存することは、実行時チェックで保証する

ということでした。

例えば「一覧画面から選択された ID なので存在するはず」という前提がある場合でも、その後、別の画面からそのデータが削除されている可能性もあるため、 ID が存在していない可能性もあります。

その場合は、 Repository や UseCase で requireNotNull() を使ったり、独自例外を投げたりして不整合を検知することになります。

型安全な設計を考える際は、「これはコンパイル時に分かる情報か?それとも実行時にしか分からない情報か?」を意識すると、どこまでを型で表現すべきか判断しやすくなります。