LoginSignup
2
5

More than 5 years have passed since last update.

enum class で状態遷移をつくる

Posted at

enum class でステートマシンもどきに
静的に状態遷移する簡単な実装をしてみようと思います。

(ステートマシンといえるのかあいまいなので もどき とします)

状態のインターフェイスを定義する

まず各状態がどう振る舞うかインターフェイスを定義してみます。

各状態は静的に次の状態への遷移を持ち、
状態に付随したなにかを行うアクションを持つと定義します:

/**
 * 状態
 * (アクションもいれちゃう)
 */
interface State {
    /** @property 次に遷移する状態 */
    val next: State?

    /** なにかする */
    fun doAction()
}

このような感じになりました。

(状態に対してどう振る舞うかのアクションは分離したほうがよさそうですが、ここでは含めます)。

各状態を実装する

次にこの状態の定義に応じて、各状態の実装を書いてみます:

/**
 * とある状態遷移
 */
enum class MyStateMap(override val next: State?): State {

    /**
     * はじめの状態
     */
    FIRST(SECOND) {
        override fun doAction() {
            println("FIRST")
        }
    },

    /**
     * 次の状態
     */
    SECOND(THIRD) {
        override fun doAction() {
            println("SECOND")
        }
    },

    /**
     * 最後の状態
     */
    THIRD(null) {
        override fun doAction() {
            println("THIRD")
        }
    }

}
状態遷移
◎ -> `FIRST` -> `SECOND` -> `THIRD` -> ×

なにかの条件にかかわらず、この順で毎回状態遷移する実装を書きました。
(ここにエラーがありますが、次に進みます)

状態遷移の実装

最後に、状態遷移を実装します:

/**
 * 状態遷移させる
 */
fun run() {
    var state: State? = MyStateMap.FIRST
    while (state !== null) {
        state.doAction()
        state = state.next
    }
}

次の状態が未定義 (null) になるまで
アクションを実行して遷移を繰り返すだけです。

定義の順番

しかしながら、このコードはエラーになります:

output
Main.kt:21:11: error: enum entry 'SECOND' is uninitialized here
    FIRST(SECOND) {
          ^
Main.kt:30:12: error: enum entry 'THIRD' is uninitialized here
    SECOND(THIRD) {
           ^

次の状態のプロパティである next
各コンストラクタで与えるようにしているところで、
FIRST を初期化している時点では SECOND が初期化されていないので
にっちもさっちもいかなくなります。

このケースでは単純に逆順に定義すればよいのですが、
依然、状態の定義順に悩まされることになります。

これを解決するためにコンストラクタでプロパティを初期化せずに
by lazy でプロパティの初期化を遅延してみます

enum class MyStateMap: State {

    /**
     * はじめの状態
     */
    FIRST {
        override val next by lazy { SECOND }
        override fun doAction() {
            println("FIRST")
        }
    },

    // 以下略
}
output
FIRST
SECOND
THIRD

いけました。

最終的に

少し変更して、最終的には次のようになります:

/**
 * 状態の定義
 */
interface State<Action> {
    /** @property 次の状態 */
    val next: State<Action>?

    /** @property アクション */
    val action: Action
}

/**
 * 初期状態の定義
 */
interface StateInitializer<State> {
    /** @property 初期状態 */
    val initialState: State
}

/** とあるアクション */
typealias MyAction = () -> Unit;

/**
 * とある状態遷移
 */
enum class MyStateMap: State<MyAction> {

    /**
     * はじめの状態
     */
    FIRST {
        override val next by lazy { SECOND }
        override val action = {
            println("FIRST")
        }
    },

    /**
     * 次の状態
     */
    SECOND {
        override val next by lazy { THIRD }
        override val action = {
            println("SECOND")
        }
    },

    /**
     * 最後の状態
     */
    THIRD {
        override val next = null
        override val action = {
            println("THIRD")
        }
    };

    companion object: StateInitializer<State<MyAction>> {
        override val initialState = FIRST
    }

}

/**
 * 状態を遷移させる
 * @param initializer 初期状態の定義
 */
fun run(initializer: StateInitializer<State<MyAction>>) {
    var state: State<MyAction>? = initializer.initialState
    while (state !== null) {
        state.action.invoke()
        state = state.next
    }
}

fun main(args: Array<String>) {
    run(MyStateMap)
}

アクションは別に分離し、
初期状態を companion class が与えるようにしました。

結論

大規模になるのであれば
結局 MVI や Flux のパターンになっていくので
普通にそれらの実装のライブラリを用いるのが懸命だと感じました。

( Android 向けも混じってます )

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5