LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック: 第 5 回(悪列挙は良層を駆逐する)

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 の使用も同様の問題を発生させます。例えば、リブランディングのために FREEPERSONALUNLIMITED という列挙子の名前を BRONZESILVERGOLD に変更したくなったとします。しかし、この変更をそのまま行ってしまうと、すでに外部で使っている値が壊れてしまいます。

この 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