iku8blog

Webエンジニアのタダのメモ。

Kotlinのsealed classの使用例・使いどころを考えてみた

f:id:iku8:20210613155736j:plain

結論から言うと、以下のような場合にメリットを享受できそう

Enumのように扱い、任意の値を保持させたい場合

Enumのように扱う?

Enumのように扱うとは何か? kotlinは最近使い始めたばかりで、間違っているかもしれないが、sealed classに関してはEnumを使う場面で使えるぽい

Enumのようなデータの持ち方をsealed classで表現できる。以下に例を示す

ここでは、何かのサービス契約タイプを表現するとする

sealed class ContractType {
    object NormalContract: ContractType()
    object GoldContract: ContractType()
    object PlatinumContract: ContractType()
}
fun main() {
    val contract: ContractType = ContractType.NormalContract
}

sealed classは同一ファイル内でしか継承出来ないというルールがあるので、Enumとして使うことができる。 取りうる値が限定できるということ。

Enumでも良いが、sealed classを使うには理由があり、任意の値を保持できるというメリットがある

使い所を考える

任意の値を保持できる

これを使って以下のようなclassを作成してみた

契約タイプによって、特定の条件でディスカウントが受けられるかどうかを判別メソッドを持つsealed class。

契約タイプは以下の3つで、ちょっとした振る舞いを持っている

  • ノーマル
    • 30歳以上且つ本人認証済みの場合、ディスカウント受けられる
  • ゴールド
    • 本人認証済みの場合、ディスカウントが受けられる
  • プラチナ
    • 必ずディスカウントが受けられる
// 契約タイプごとにディスカウントになるための条件が異なっており、契約タイプ作成時にタイプによって引数が変わる
sealed class ContractType {
    abstract fun canDiscount(): Boolean

    data class NormalContract(val age: Int, val identityVerified: Boolean): ContractType() {
        companion object {
            const val DISCOUNT_TARGET_AGE = 30
        }

        override fun canDiscount(): Boolean {
            if(DISCOUNT_TARGET_AGE <= age && identityVerified) {
                return true
            }
            return false
        }
    }

    data class GoldContract(val identityVerified: Boolean): ContractType() {
        override fun canDiscount(): Boolean = identityVerified
    }

    object PlatinumContract: ContractType() {
        override fun canDiscount(): Boolean = true
    }
}

ノーマルとゴールドの場合、ディスカウント条件になる値を受け取る必要があるため、data classとしてタイプを宣言しておく。このように引数をとることで、Enumぽく、任意の値を保持することに成功した。

プラチナの場合は無条件でディスカウントが受けられるので、引数を取る必要はなくなり、objectとしてタイプを宣言した。

使用例

fun main() {
    val contractNormal1: ContractType = ContractType.NormalContract(25, false)
    val contractNormal2: ContractType = ContractType.NormalContract(38, true)
    println("ディスカウントnormal1: ${contractNormal1.canDiscount()}")
    println("ディスカウントnormal2: ${contractNormal2.canDiscount()}")

    val contractGold1: ContractType = ContractType.GoldContract(false)
    val contractGold2: ContractType = ContractType.GoldContract(true)
    println("ディスカウントgold1: ${contractGold1.canDiscount()}")
    println("ディスカウントgold2: ${contractGold2.canDiscount()}")

    val contractPlatinum: ContractType = ContractType.GoldContract(true)
    println("ディスカウントplatinum: ${contractPlatinum.canDiscount()}")

実行結果は以下の通り。

f:id:iku8:20210613162104p:plain

このような実装にすることで、Enumのように扱うことができ、なおかつ特定のタイプの場合は引数を取ることで、様々な振る舞いをタイプに対して実装することができる。

もしEnumクラスを使うのであれば、タイプごとに引数を取ることは出来ず、Enumを受け取れるような新たなクラスを作成し、その内部実装でタイプごとに振る舞いを変えるなどする必要がある。

今回の使用例であれば、Typeを自ら指定しているので引数の指定は苦労していないが、TypeをDBなどから取り出す場合は、どういう引数を取るべきかわからないので、Typeごとに引数の値をDB等から取得してTypeに渡してあげるという作業が発生する。

個人的には、Enumのような値にはなるべくビジネスロジックを持たせない方が良いと思っているので、sealed classを使う場面は今の所あまりないかもしれない。ビジネスロジックを持ってしまうと、ドメインモデルが存在する場合そこと分散してしまうから。ちょっとしたことなら良いかも。

例えば、今回の例だとディスカウントを受けられるかどうかというのを、ContractType内に実装しているが、 ディスカウント以外に、このタイプであればこれを返すなどといった振る舞いが増えた場合、ContractTypeは肥大がしてしまうので、用途ごとにEnumを受け取れるようなクラスを作成して、タイプごとに振る舞いを変えるというのが良いと思っている。

ContractTypeのような値が、かなり限定的に使われ、振る舞いを多く持たないのであれば、sealed classは有効なんじゃないかと思っている。

初めて使った感想なので、こんなんもあるよという意見があれば伺いたい。