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
) になるまで
アクションを実行して遷移を繰り返すだけです。
定義の順番
しかしながら、このコードはエラーになります:
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")
}
},
// 以下略
}
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 のパターンになっていくので
普通にそれらの実装のライブラリを用いるのが懸命だと感じました。
- https://github.com/Tinder/StateMachine
- https://github.com/spotify/mobius
- https://github.com/freeletics/RxRedux
- https://github.com/freeletics/CoRedux
- https://github.com/badoo/MVICore
( Android 向けも混じってます )