🐾

Kotlinで前方互換性をもった列挙可能な型を定義する

2024/07/13に公開

列挙型と前方互換性

Kotlinでコードを書いていてよく遭遇する問題のひとつに、「Web API の JSON に含まれる列挙型のとる値がある日突然増えたせいで JSON のパースができなくなって正常に動作しなくなる」というものがあります。
この問題は多くの場合 JSON に含まれるフィールドをシンプルに enum class として扱うような設計になっていることに起因して発生します。設計的に前方互換性が考慮できていないということですね。
こうした問題を起こさないように前方互換性を維持できるような列挙可能な型を定義することはできないでしょうか?

列挙可能 = 網羅性

「列挙可能であるか?」というのは Kotlin の世界では「exhaustive when として扱えるか?」と置き換えることができます。

網羅性のある前方互換性を持った型

Kotlin で exhaustive when となればおおよその検討がつくかと思いますが、予想通り下記のような型を定義することで前方互換性を持たせることができます。

sealed interface SaleType {
    enum class Defined : SaleType {
        SEASONAL,
        TIME_LIMITED,
        AREA_LIMITED;
    }

    data class Undefined(
        val rawValue: String,
    ) : SaleType

    companion object {
        fun of(value: String): SaleType = Defined.values().find { it.rawValue == value } ?: Undefined(value)
    }
}

このような型を定義して JSON から取り出した文字列を変換する(Moshi のようなよく知られた JSON パーサーを使っているならカスタム Adapter を定義しても良いと思います)ことで未定義の値が来たときは Undefined として扱い、既知の値が来たときは Defined のいずれかのフィールドの値として扱うことができます。

when (saleType) {
    SaleType.Defined.SEASONAL -> setLabel("季節限定セール")
    SaleType.Defined.TIME_LIMITED -> setLabel("期間限定セール")
    SaleType.Defined.AREA_LIMITED -> setLabel("地域限定セール")
    is Undefined -> {
        logger.w("Undefined saleType is detected: ${saleType.rawValue}")
        setLabel("特別セール")
    }
}

例えばこんな感じで「将来よくわからない値が来たらとりあえず「特別セール」と表示することで最低限の前方互換性を持たせよう」といった実装ができるようになります。

Discussion