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
に変更したくなったとします。しかし、この変更をそのまま行ってしまうと、すでに外部で使っている値が壊れてしまいます。
この ordinal
や name
の恐ろしい点は、列挙型を使う側の都合で簡単なリファクタリングを行っただけのつもりでも、実際にはバグを発生させてしまうという点です。列挙型を使う側の視点では、外部で使う値に影響するとは一見では分かりにくいでしょう。
腐らせないようラップをかける
この問題を解決するためには、外部で使う値と列挙子の宣言を独立させると良いでしょう。以下のように、外部で使う値を列挙型のプロパティとして定義する方法があります。
enum class AccountType(val dbValue: String) {
FREE("free"),
PERSONAL("personal"),
UNLIMITED("unlimited");
companion object {
val DB_VALUE_TO_TYPE_MAP: Map<String, AccountType> =
values().associateBy(AccountType::dbValue)
}
}
このようにすることで、AccountType
の列挙子の名前の変更や順序の変更から、外部で使う値を保護することができます。
AccountType
を使うコードから dbValue
を隠す必要がある場合は、コンバータとなるクラスを別に定義し、そこで dbValue
とDB_VALUE_TO_TYPE_MAP
に相当する関数・プロパティを定義すると良いでしょう。
例外: その場で食べれば腐らない
列挙型を外部で使う値に変換せず、あくまでも「一時的な変換」として使う場合ならば、name
や ordinal
を使っても問題が起きにくいです。例えば、オンメモリのキャッシュとして列挙子ではなく、整数を保存する場合などが考えられます。ただしその場合は、型安全性の恩恵を受けるためにも、可能な限り name
や ordinal
ではなく、列挙子そのものを使うようにしてください。
一言まとめ
外部で定義された値を変換するコードでは、内部の値と外部の値を互いに独立させて定義する。
キーワード: value conversion
, externally defined value
, enum