LINEヤフー Advent Calendar 2023の7日目の記事です。
こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 5 回です。Weekly Report については、第 1 回の記 事を参照してください。
悪列挙は良層を駆逐する
あるサービスの「ユーザアカウントの種別」として、以下のような列挙型が定義されていると仮定します。
enum class AccountType { FREE, PERSONAL, UNLIMITED }
この値について、ローカルストレージやデータベース、ネットワーク越しの API を使って読み書きを行う場合、「コンバータ」や「マッパー」と呼ばれる仕組みを使って、言語固有のオブジェクトとインターフェース定義言語やプロトコルで定義されたバイト列で相互変換することがあります。
例えば、Android アプリケーションの開発では Room という永続化ライブラリを利用でき、TypeConverter
という仕組みを使って、簡単にコンバータを実装することができます。
しかし、以下の TypeConverter
の利用例には問題があります。それはどういった点でしょうか?
class AccountTypeConverter {
@TypeConverter
fun fromStringValue(typeString: String): AccountType = AccountType.valueOf(typeString)
@TypeConverter
fun toStringValue(type: AccountType): String = type.name
}
class AccountTypeConverter {
@TypeConverter
fun fromIntValue(typeInt: Int): AccountType = type.values()[typeInt]
@TypeConverter
fun toIntValue(type: AccountType): Int = type.ordinal
}
腐敗防止層としてのコンバータ
このコードの問題点は、name
と ordinal
という列挙型のプロパティを使うことにより、コンバータが腐敗防止層の役割を果たしていないという点です。
この問題により、外部(インターフェース定義言語やプロトコル)の変更が列挙型を使うコードに影響を与えますし、その逆もまた然りです。もし、データベースやリモート API で使う値(以下、外部で使う値)に変更が起きた場合、それは列挙子の定義の変更を伴うため、列挙子を使う側のコードにも影響が及びます。一方で、列挙型を使う側の都合で、列挙子に別名を与えたり、定義順を変えたくなることもあります。しかし、名前や順番の変更は外部で使う値に直接影響を及ぼすため、自由に行うことができません。
ordinal
を使うことで起きる具体的な問題を例に挙げます。今、新しい AccountType
として BUSINESS
を追加したくなったとしましょう。価格設定や機能の面から考えて BUSINESS
は PERSONAL
と UNLIMITED
の中間に定義することが妥当であるとします。
enum class AccountType { FREE, PERSONAL, BUSINESS, UNLIMITED }
しかし、この変更は UNLIMITED.ordinal
の値を変更してしまいます。そのため、この変更を行うためには外部で使う値も更新しなければなりません。
また、name
の使用も同様の問題を発生させます。例えば、リブランディングのために FREE
・PERSONAL
・UNLIMITED
という列挙子の名前を BRONZE
・SILVER
・GOLD