[Android] enum classのSetを保存するconverterをTemplate methodパターンで効率よく実装

この記事はRoomを使ってenum classのSetを保存するconverterを効率良く実装する方法について書いてあります。
ソースコードは[こちら](https://github.com/shiita0903/EnumSetTypeConverter)

## サンプルコード
適当なデータ(`HogeEntity`)をRoomのデータベースに保存することにします。`HogeEntity`のプロパティである`animals`と`seasons`はenum classのSetです。

```kotlin
@Entity
data class HogeEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val animals: Set<Animal>,
    val seasons: Set<Season>
)

enum class Animal {
    DOG, CAT, BIRD
}

enum class Season {
    SPRING, SUMMER, AUTUMN, WINTER
}
```

DatabaseとDAOを以下のように実装します。

```kotlin
@Database(entities = [HogeEntity::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
    abstract fun hogeDao(): HogeDao

    companion object {
        private const val dbName = "my.db"

        fun createDbInstance(context: Context): MyDatabase = Room
            .databaseBuilder(context, MyDatabase::class.java, dbName)
            .fallbackToDestructiveMigration()
            .build()
    }
}
```

```kotlin
@Dao
interface HogeDao {
    @Query("SELECT * FROM HogeEntity")
    fun findAll(): List<HogeEntity>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun upsert(hogeEntity: HogeEntity)
}
```

この状態でビルドすると、

```
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
```

というエラーが出ます。これは`HogeEntity`のプロパティの`animals`と`seasons`の型がサポートされていないために出るエラーで、指示通りにconverterを実装することで解決します。


## Converter
enum classのSetのためのconverterを実装する前に、データベースに保存できる型にどのように変換するかを考えます。Setの要素(enum)の一つ一つを2進数のあるビットに紐づけ、Setに含まれているならば`1`、そうでないならば`0`とすることで、`Int`で表現できそうです。
例えば、`Set<Animal>`は以下のように`Int`に変換することができます。

|`Set<Animal>`|`Int`(2進数表示)|`Int`(10進数表示)|
|:--|:--:|:--:|
|[]|0b000|0|
|[DOG]|0b001|1|
|[CAT]|0b010|2|
|[CAT, DOG]|0b011|3|
|[BIRD]|0b100|4|
|[BIRD, DOG]|0b101|5|
|[BIRD, CAT]|0b110|6|
|[BIRD, CAT, DOG]|0b111|7|

### 実装
方針が決定したので、`HogeEntity`のプロパティの`animals`と`seasons`の型である`Set<Animal>`と`Set<Season>`を変換するconverterを実装していきます。これらの2つのconverterは似たような処理が多いので、Template methodパターンを利用して共通化します。

```kotlin
abstract class EnumSetConverters<T : Enum<T>>(private val clazz: Class<T>) {
    protected abstract val T.storeBit: Int

    @TypeConverter
    fun toDbValue(enums: Set<T>?): Int? = enums?.sumBy { 1 shl it.storeBit }

    @TypeConverter
    fun fromDbValue(dbValue: Int?): Set<T>? {
        dbValue ?: return null
        return clazz.enumConstants
            ?.filter { dbValue ushr it.storeBit and 1 == 1 }
            ?.toSet()
    }
}
```

AbstractClassである`EnumSetConverters`は共通化したい2つのメソッド(`toDbValue()`と`fromDbValue()`)と、それぞれのconverterで異なる処理をしたいprotectedな拡張プロパティ(`storeBit`)で構成されています。ConcreteClassである2つのconverterの実装は以下のようになります

```kotlin
class AnimalConverters : EnumSetConverters<Animal>(Animal::class.java) {
    override val Animal.storeBit: Int
        get() = when (this) {
            Animal.DOG -> 0
            Animal.CAT -> 1
            Animal.BIRD -> 2
        }
}
```

```kotlin
class SeasonConverters : EnumSetConverters<Season>(Season::class.java) {
    override val Season.storeBit: Int
        get() = when (this) {
            Season.SPRING -> 0
            Season.SUMMER -> 1
            Season.AUTUMN -> 2
            Season.WINTER -> 3
        }
}
```

このようにTemplate methodパターンを利用することで、新しいenum classのSetのconverterを追加する時には、そのconverter固有の処理である**enumをどのビットに紐づけるかを実装するだけ**で良くなります。

## 正しく動くことを確認
作成した2つのconverterをRoomのデータベースに設定し

```kotlin
@Database(entities = [HogeEntity::class], version = 1)
@TypeConverters(AnimalConverters::class, SeasonConverters::class)
abstract class MyDatabase : RoomDatabase() {
    ...
}
```

保存した後にロードして確認します。

```kotlin
hogeDao.upsert(
    HogeEntity(
        id = 0,
        animals = setOf(Animal.DOG, Animal.BIRD),
        seasons = setOf(Season.SUMMER, Season.WINTER)
    )
)
hogeDao.upsert(
    HogeEntity(
        id = 0,
        animals = emptySet(),
        seasons = Season.values().toSet()
    )
)

Log.d("hoge list", hogeDao.findAll().toString())
```

ログの出力は以下の通りで、問題なく保存されていることを確認することができました。

```
D/hoge list: [HogeEntity(id=1, animals=[DOG, BIRD], seasons=[SUMMER, WINTER]), HogeEntity(id=2, animals=[], seasons=[SPRING, SUMMER, AUTUMN, WINTER])]
```

## おわりに
今回はTemplate methodパターンを使ってRoomのconverterを効率よく実装しました。デザインパターンはうまく使うと非常に強力なので積極的に活用していきたいですね。