UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【Kotlin】シールドクラス の使い所

JavaにもJava17から シールドクラス が導入されたが、Kotlin にもあることを知った

シールドクラスは特定のサブクラスのみが許可され、外部の任意のクラスが継承することができないようにする機能である

シールドクラス の概要は知っていたが、そもそもどういう時に使うのが良いかがわかっていなかったので調べた

使うべきかの判断材料

シールドクラス を使うべきかどうかの判断材料は、主にデータや型を厳密に制限し、コンパイル時にすべてのケースを把握して安全性を確保したい場合に基づく

例えば、以下のような場合

1. 型の集合を限定したい場合

シールドクラスを使うべき最大の理由は、型の集合を明示的に限定したいとき

判断基準:

  • モデリングする概念が限られたケース(例: 状態、イベント、コマンドなど)しかない場合
  • これ以上新しいサブクラスが増えないことがわかっている場合

例:

操作の結果モデリングする際、成功と失敗という決まったパターンしかない場合は、シールドクラスが適している

sealed class Result
data class Success(val data: String) : Result()
data class Failure(val error: Throwable) : Result()

ここでは、SuccessFailure 以外の Result は存在しないことが保証される

2. パターンマッチングの完全性を保証したい場合

シールドクラスの大きな利点は、when を使ったパターンマッチング時に、すべてのケースがコンパイル時にチェックされること

つまり、シールドクラスを使うことで、パターンマッチの完全性を保証でき、else を書く必要がなくなる

判断基準:

  • すべてのサブクラスを exhaust(網羅)する処理が必要で、追加のケースを安全に処理したい場合
  • コンパイル時に未処理のケースがある場合にエラーを発生させたい場合

例:

fun handleResult(result: Result) {
    when (result) {
        is Success -> println("Data: ${result.data}")
        is Failure -> println("Error: ${result.error}")
        // else ブロックが不要、他のサブクラスが存在しないことが保証されている
    }
}

新しいサブクラスが追加された場合、コンパイラは未処理のケースを警告するため、見落としを防ぐことができる

3. クローズドな階層構造が必要な場合

シールドクラスはその階層構造が閉じているので、クラスの継承関係を制限できる

判断基準:

  • サブクラスの追加や継承を制御し、拡張性を制限したい場合
  • クラスのインスタンスが決まった種類にしかならないことを保証したい場合

例:

ユーザーがアクションを選択できる UI をモデリングする場合、操作が固定されているとき

sealed class UserAction
object Click : UserAction()
object Swipe : UserAction()
object Drag : UserAction()

UserAction の種類は Click, Swipe, Drag に限定され、それ以外のアクションが追加されることはない

4. 状態やイベントをモデリングする場合

シールドクラスは、状態遷移が決まっているシステム(例えば、UI の画面状態、アプリケーションのライフサイクル、ネットワークのリクエストの状態など)の有限の状態イベントを表現するのに適している

判断基準:

  • 状態遷移やイベントが明確に定義されており、これ以上増えないことが分かっている場合
  • システムが有限の状態やイベントに基づいて動作する場合

例:

アプリの状態を表現する場合

sealed class AppState
object Loading : AppState()
data class Loaded(val data: List<String>) : AppState()
object Error : AppState()

状態が Loading, Loaded, Error に限定されており、他の状態はないことが保証される

5. 代数的データ型(ADT)の実現

Kotlin のシールドクラスは、関数型プログラミングにおける 代数的データ型(Algebraic Data Types, ADT) のようなもの

ADT は、複数のサブタイプを持つ「選択肢型(sum types)」として動作します。シールドクラスを使うことで、これを実現できる

判断基準:

  • 複数の固定されたオプション(サブタイプ)を持ち、それらをすべて処理したい場合
  • ADT 形式でデータ構造を定義したい場合

例:

関数型プログラミングでよく使われる Option 型がシールドクラスで実装できる

sealed class Option<out T>
data class Some<out T>(val value: T) : Option<T>()
object None : Option<Nothing>()

Option 型には SomeNone という2つのケースがあり、それ以上のケースがないことが保証される

まとめ

なんとなく、Enumのような使い方ができるのかなと感じた

シールドクラスを使うかどうかの判断は、主に以下の質問に基づいてできそう

  • クラスのサブタイプを明確に限定したいか?
  • サブクラスが固定されている場合、すべてのパターンを網羅する安全な処理が必要か?
  • 将来的にサブクラスの追加が不要か、追加が許されない設計か?

これらに該当する場合、シールドクラスを使うのが良さそう