Android
Kotlin
enum
委譲

Kotlinで委譲:enumに委譲することでenumの機能を拡張する

Kotlinでは実装を他のクラスへ委譲(Class Delegation)することができます。
これは大変便利です。が、コードを委譲先へ共通化するとかクラスの継承を避けるために使われることが多いのではないでしょうか?
この記事では目先を変えて、委譲を使ってEnumの機能を拡張してみます。

※この記事はファイアーエムブレムヒーローズの戦闘結果計算ツールをKotlinでDDD的に作ってみたの補足でもあります。

Enum

Kotlinに限らずJavaでもEnumは大変に便利です。
初期化したりメソッドを追加したりできます。

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

ですが、初期化できるだけなので新しいパラメータを与えることはできませんし、新しい値のEnumを作ることもできません。よって「種類は同じだが程度が違うEnum」は作れません。ですが、ゲームでは「特定のスキルをある程度のレベルで持つ」事がよくあります。そこで、(若干安全性に欠けますが)柔軟なEnumを作ってみます。

enum class Skill(val level: Int) {
    //鬼神の一撃(level)の一つで済ませたい
    鬼神の一撃_1(1),
    鬼神の一撃_2(2),
    鬼神の一撃_3(3),
}

公式での委譲の説明

公式では"指定されたオブジェクトへの public メソッドのすべてを委譲することができます。"として説明しています。

//ほぼ公式のコード
interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

class EnumTest {
    @Test
    fun mainTest() {
        val b = BaseImpl(10)
        Derived(b).print() // 出力:10
    }
}

Enumにする

BaseImplをEnumにするとこうなります。

interface Base2 {
    val x: Int get() = 0
    fun printX() {
        print(x)
    }
}

enum class BaseImpl2(override val x: Int) : Base2 {
    BaseImpl2A(10);
}

class Derived2(b: Base2) : Base2 by b

class EnumTest2 {
    @Test
    fun mainTest() {
        val b = BaseImpl2.BaseImpl2A
        b.printX() // 出力:10
        Derived2(b).printX() // 出力:10
    }
}

無事Enumに委譲されました。
ここで、委譲元であるDerived2でプロパティをOverrideして任意の数字を出力させるようにします。

//期待通りに動作しないコード
interface Base2 {
    val x: Int get() = 0
    fun printX() {
        print(x)
    }
}

enum class BaseImpl2(override val x: Int) : Base2 {
    BaseImpl2A(10);
}

class Derived2(b: Base2,override val x: Int) : Base2 by b

class Derived2B(b: Base2,override val x: Int) : Base2

class EnumTest2 {
    @Test
    fun mainTest() {
        val b = BaseImpl2.BaseImpl2A
        b.printX() // 出力:10
        Derived2(b,30).printX() // 出力:30ではなく10
        Derived2B(b,30).printX() // 出力:30
    }
}


Derived2のプロパティが無視されてしまいました。そもそも継承で作ったDerived2Bと挙動が異なります。
良く継承の代わりに委譲を使え、と言いますが継承と委譲は完全に別のものです。委譲先はあくまで別のオブジェクトなので委譲元と委譲先のプロパティは異なります。ついでに言うと、委譲ではTemplate Methodパターンとかも書き方が変わります。

そこで、Interfaceの関数にデフォルト引数としてプロパティを追加します。

interface Base3 {
    val x: Int get() = 0
    fun printX(i:Int = x) {
        print(i)
    }
}

enum class BaseImpl3(override val x: Int) : Base3 {
    BaseImpleA(10);
}

class Derived3(b: Base3,override val x: Int) : Base3 by b

class EnumTest3 {
    @Test
    fun mainTest() {
        val b = BaseImpl3.BaseImpl3A
        b.printX() // 出力:10
        Derived3(b,30).printX() // 出力:30
    }
}

Interfaceの関数はpublicなので委譲経路の一番外側、つまりDerived3のプロパティを参照します。これにより、期待通り後から与えられたパラメータを使う事が出来ました!

冒頭に書いたアプリでは実際にこんな感じでスキルごとに能力、つまりメソッドを与え、そこにレベルを後から追加しています。

interface Base4 {
    val x: Int get() = 0
    fun printX(i: Int = x) {
        print(i)
    }
}

enum class BaseImpl4(override val x: Int) : Base4 {
    鬼神の一撃(10) {
        override fun printX(i: Int) {
            print("攻撃+${i * 2}")
        }
    },
    飛燕の一撃(10) {
        override fun printX(i: Int) {
            print("速さ+${i * 2}")
        }
    },
}

class Derived4(b: Base4, override val x: Int) : Base4 by b

class EnumTest4 {
    @Test
    fun mainTest() {
        val b = BaseImpl4.鬼神の一撃
        Derived4(b, 3).printX() // 出力:攻撃+6
        Derived4(BaseImpl4.飛燕の一撃, 3).printX() // 出力:速さ+6
    }
}

これでとても柔軟なEnumになりました。ただしDerived4のequalsはenumとレベルも見るように書き直す必要があります。とはいえ、このDerived4は不変であるEnumと不変であるIntのxがともに等しければ同じであり、良く正確であるか問題となるequalsも簡単に書くことができます。

class Derived5(val b: Base5, override val x: Int) : Base5 by b {
    override fun equals(other: Any?): Boolean = other is Derived5 && other.b == this.b && other.x == this.x
}

Enumの嬉しいところ

さて、このEnumですが何が嬉しいのでしょうか?実を言うとEnumであること自体が嬉しかったりします。具体的に言えば既に生成されているのでクラスに言及する必要がありません。

class EnumTes5 {
    @Test
    fun mainTest() {
        val b = BaseImpl5.valueOf("鬼神の一撃")
        Derived5(b, 3).printX() // 出力:攻撃+6
        Derived5(BaseImpl5.valueOf("飛燕の一撃"), 3).printX() // 出力:速さ+6
        println(Derived5(b, 3) == Derived5(BaseImpl5.valueOf("鬼神の一撃"), 3))//true
        print(Derived5(b, 1) == Derived5(BaseImpl5.valueOf("鬼神の一撃"), 3))//false
        print(Derived5(b, 1) == Derived5(BaseImpl5.valueOf("飛燕の一撃"), 3))//false
    }
}

選択したスキルを取得したりする場合、スキルを生成する必要があります。つまり、ロジック内であれファクトリに隔離するであれ、どこかにクラス名を書かなければなりません。
どこかにクラス名を書くという事は、アップデート時にスキルが増えたりしたらそこに書き足す必要があるという事です。
ですがEnumならvalueOf()だけでOKです。スキルが増えたら単にEnumを増やせば済みます。スキルのリストもわざわざ作る必要はなく、BaseImpl5.values()で得たリストをそのまま使っても良し、職業によってフィルタしても良しとすこぶる使い勝手がよくなります。

冒頭で紹介したアプリでは、全てのスキルをEnumにして画面で選択するときやリポジトリに保存するときは単にnameに変換し、スキルに戻すときはvalueOf()で戻しています。変換ロジックはミスが入りやすいところであると同時にドメインロジックが漏れやすいところでもあるのでそれを防ぐことができるのは大きなメリットです。

まとめ

委譲は便利
Enumも便利
委譲とEnumを組み合わせるととても便利
ただし委譲と継承は別の物なので同じように使ってはいけない
Enumの安全性が一部失われるので気を付けて使う必要がある