🤐

Kotlin の sealed class で enum class の values(), valueOf() が欲しいを解決する

2021/12/11に公開

この記事は、「イエソド アウトプット筋 トレーニング Advent Calendar 2021」の10日目の記事です。

sealed class を使って列挙を表現したときに、 enum class にある values(), valueOf() 等のメソッドが欲しいときに使える、インターフェースと拡張関数の組み合わせを紹介します。

sealed class を列挙に使うって、例えばどういう時に使うの?

たとえば、ValueObjectを使っているときに、特定の値の塊に意味を持たせたい場合に使えます。 enum class はValueObjectのクラスを継承できませんが、 sealed class は継承できます。

open class AttributeId<out T>(val uuid: UUID)
data class ReferenceEntityId(val uuid: UUID)

sealed class ReferenceAttributeId(uuid: UUID) :
    AttributeId<ReferenceEntityId>(uuid) {

    object COMPANY : ReferenceAttributeId(UUID(0, 1))
    object ORGANIZATION : ReferenceAttributeId(UUID(0, 2))
    object OFFICE : ReferenceAttributeId(UUID(0, 3))
    object PROJECT : ReferenceAttributeId(UUID(0, 4))
}

sealed class なので、例えば下のようなwhen式の中に else -> を書かなくてよく、もし ReferenceAttributeId に新しく項目を追加したときには、when式に項目が足りないとコンパイラが怒ってくれます。

fun toJapaneseLabel(referenceAttributeId: ReferenceAttributeId): String =
    when (referenceAttributeId) {
        ReferenceAttributeId.COMPANY -> "会社"
        ReferenceAttributeId.ORGANIZATION -> "組織"
        ReferenceAttributeId.OFFICE -> "オフィス"
        ReferenceAttributeId.PROJECT -> "プロジェクト"
    }

values(), valueOf() のために、とりあえずこんな感じの物を用意する

インターフェース名は適当です。計算量も適当なのでヘビーな環境だとつらいかもしれません(改良の余地はあります)。

interface SealedClassEnumExtension<T>

interface SealedClassEnumWithName {
    val name get(): String = this::class.simpleName ?: "N/A"
}

inline fun <reified T> SealedClassEnumExtension<T>.values(): List<T> {
    return T::class.sealedSubclasses.mapNotNull { it.objectInstance }
}

inline fun <reified T : SealedClassEnumWithName> SealedClassEnumExtension<T>.valueOf(name: String): T {
    return values().find { it.name == name } ?: throw IllegalArgumentException(name)
}

上のインターフェースと拡張関数をどう適用するの?

sealed classSealedClassEnumWithName をインターフェースとして指定し、 その companion objectSealedClassEnumExtension<T> を継承します。

.namevalueOf() が必要無ければ、 SealedClassEnumWithName をインターフェースに指定する必要はありません。

open class AttributeId<out T>(val uuid: UUID)
data class ReferenceEntityId(val uuid: UUID)

sealed class ReferenceAttributeId(uuid: UUID) :
    AttributeId<ReferenceEntityId>(uuid), SealedClassEnumWithName {

    object COMPANY : ReferenceAttributeId(UUID(0, 1))
    object ORGANIZATION : ReferenceAttributeId(UUID(0, 2))
    object OFFICE : ReferenceAttributeId(UUID(0, 3))
    object PROJECT : ReferenceAttributeId(UUID(0, 4))

    companion object : SealedClassEnumExtension<ReferenceAttributeId>
}

どんなかんじでつかえるの?

こんなかんじです。

fun main() {
    println(ReferenceAttributeId.values().map { it.name })
    // output: [COMPANY, ORGANIZATION, OFFICE, PROJECT]

    println(ReferenceAttributeId.valueOf("COMPANY").name)
    // output: COMPANY

    fun toJapaneseLabel(referenceAttributeId: ReferenceAttributeId): String =
        when (referenceAttributeId) {
            ReferenceAttributeId.COMPANY -> "会社"
            ReferenceAttributeId.ORGANIZATION -> "組織"
            ReferenceAttributeId.OFFICE -> "オフィス"
            ReferenceAttributeId.PROJECT -> "プロジェクト"
        }

    println(toJapaneseLabel(ReferenceAttributeId.COMPANY))
    // output: 会社
}

まとめ

いかがでしたでしょうか?

かゆい

うま

Discussion