C# 状態遷移定義クラス
説明
StateVectorと言う名前で公開したクラスは、状態遷移関連の定義を簡素化し、集約するために作成しました。
https://github.com/visyeii/StateVector
定義はこんな感じです。
https://github.com/visyeii/StateVector/blob/master/StateVector/StateVector/Form1.cs
※"() => {...}"
の箇所はラムダ式で定義した関数です。
using VE = VectorEvent;//実際はnamespace直後に定義しています。
StateVector m_stateVector;
private void Form1_Load(object sender, EventArgs e)
{
VE[] list = {
// 直前の状態(head) 新しい状態(tail) 関数(func)
new VE("init", VE.TailOr("a", "b", "c"), InitState, () => { SetLog("!"); }),
new VE("a", "b", () => { SetLog("a->b"); }),
new VE("b", "a", () => { SetLog("b->a"); }),
new VE("a", "a", () => { SetLog("a->a"); }),
new VE("b", "b", () => { SetLog("b->b"); }),
new VE(VE.HeadOr("a", "b"), "c", () => { SetLog("a|b->c"); }),
new VE("c", VE.TailOr("a", "b"), () => { SetLog("c->a|b"); })
};
m_stateVector = new StateVector("init", list);//初期状態、状態遷移の定義
}
動作
m_stateVectorの持つ値が、__直前の状態(head)__から、__新しい状態(tail)__に変化した(一致した)とき、
__関数(func)__を呼び出します。
状態の更新方法
Refreshを呼び出し、引数に新しい状態(tail)を与えます。
private void button1_Click(object sender, EventArgs e)
{
m_stateVector.Refresh("a");//新しい状態(tail)
}
private void button2_Click(object sender, EventArgs e)
{
m_stateVector.Refresh("b");//新しい状態(tail)
}
private void button3_Click(object sender, EventArgs e)
{
m_stateVector.Refresh("c");//新しい状態(tail)
}
状態遷移図(PlantUMLで生成)
下図の通り、遷移を示す矢印に関数を割り当てています。
PlantUML定義
@startuml
[*] --> Form : Form Load
state Form {
init --> a : start !
init --> b : start !
init --> c : start !
a --> b : a->b
b --> a : b->a
a --> a : a->a
b --> b : b->b
a --> c : a|b->c
b --> c : a|b->c
c --> a : c->a|b
c --> b : c->a|b
}
Form --> [*] : Form Close
@enduml
GUI表示
初期状態"init"
からの状態変化に対応する定義は次の1行です。
new VE("init", VE.TailOr("a", "b", "c"), InitState, () => { SetLog("!"); }),
省略せずに記述すると次の6行となります。
new VE("init", "a", InitState),
new VE("init", "a", () => { SetLog("!"); }),
new VE("init", "b", InitState),
new VE("init", "b", () => { SetLog("!"); }),
new VE("init", "c", InitState),
new VE("init", "c", () => { SetLog("!"); }),
※InitState
は() => { SetLog("start"); }
と同じです。
このようにVE.TailOr
ラッパーで定義を集約する事により、記述量を減らしています。DRY!
各種ラッパー
-
VE.HeadOr
・・・直前の状態(head)の定義を集約する。集約する場合は必須。 -
VE.TailOr
・・・新しい状態(tail)の定義を集約する。集約する場合は必須。 -
VE.Func
・・・関数を明示する(ラムダ式は勘違いしやすそうなので)省略可能。 -
VE.FuncArray
・・・関数の定義を集約する。集約する場合でも省略可能。
初期状態"init"
からの状態変化について、動作を変更せず、丁寧に書き直すと次の記述になります。
new VE(
VE.HeadOr("init"),
VE.TailOr("a", "b", "c"),
VE.FuncArray(InitState, () => { SetLog("!"); })
),
高度な定義
一致判定に正規表現が使用可能です。
上記Form1_Load内の定義を次のように書き換える事が出来ます。
private void Form1_Load(object sender, EventArgs e)
{
VE[] list = {
new VE("init", "[a-c]", InitState, () => { SetLog("!"); }),
new VE("a", "b", () => { SetLog("a->b"); }),
new VE("b", "a", () => { SetLog("b->a"); }),
new VE("a", "a", () => { SetLog("a->a"); }),
new VE("b", "b", () => { SetLog("b->b"); }),
new VE("a|b", "c", () => { SetLog("a|b->c"); }),
new VE("c", "a|b", () => { SetLog("c->a|b"); })
};
m_stateVector = new StateVector("init", list);
m_stateVector.EnableRegexp = true;//<-必須!!
}
※m_stateVector.EnableRegexp = true;
の記述が必要となります。
※各種ラッパーについてはそのまま使用可能です。
正規表現を使用する事により、__直前の状態(head)__または__新しい状態(tail)__に、__何にでも一致する定義__を作成可能となります。
状態の名前に接頭辞や接尾辞を付加することにより、パターンマッチを行う方法も考えられます。
より広範囲の処理を集約する事が出来ます。DRY!!
###デバッグ機能
private void Form1_Load(object sender, EventArgs e)
{
VE[] list = {
new VE("init", VE.TailOr("a", "b", "c"), InitState, () => { SetLog("!"); }),
new VE("a", "b", () => { SetLog("a->b"); }),
new VE("b", "a", () => { SetLog("b->a"); }),
new VE("a", "a", () => { SetLog("a->a"); }),
new VE("b", "b", () => { SetLog("b->b"); }),
new VE(VE.HeadOr("a", "b"), "c", () => { SetLog("a|b->c"); }),
//"tagName"で一致条件を識別可能
new VE("c", VE.TailOr("a", "b"), "tagName", () => { SetLog("c->a|b"); })
};
m_stateVector = new StateVector("init", list);
m_stateVector.GetListInfo();//一致条件一覧をデバッグ出力
m_stateVector.EnableRefreshTrace = true;//一致条件実行ログのデバッグ出力を有効化
}
ラムダ式部分は<Form1_Load>b__3_x
と言う関数名になるようです。
#一致条件一覧部分 list[配列添え字番号].priority(実行優先度)・・・値が小さいものから実行する
#各種ラッパー部分は、コンストラクタ実行時に組み合わせを展開して一致判定しています。
:list[0].priority(0) init -> a , InitState
:list[0].priority(1) init -> a , <Form1_Load>b__3_0
:list[0].priority(2) init -> b , InitState
:list[0].priority(3) init -> b , <Form1_Load>b__3_0
:list[0].priority(4) init -> c , InitState
:list[0].priority(5) init -> c , <Form1_Load>b__3_0
:list[1].priority(6) a -> b , <Form1_Load>b__3_1
:list[2].priority(7) b -> a , <Form1_Load>b__3_2
:list[3].priority(8) a -> a , <Form1_Load>b__3_3
:list[4].priority(9) b -> b , <Form1_Load>b__3_4
:list[5].priority(10) a -> c , <Form1_Load>b__3_5
:list[5].priority(11) b -> c , <Form1_Load>b__3_5
#一致条件一覧部分
#識別用にtagNameを出力します
:tagName list[6].priority(12) c -> a , <Form1_Load>b__3_6
:tagName list[6].priority(13) c -> b , <Form1_Load>b__3_6
#実行ログ
init -> a do[0].priority(0) InitState done.
init -> a do[0].priority(1) <Form1_Load>b__3_0 done.
a -> b do[1].priority(6) <Form1_Load>b__3_1 done.
b -> c do[5].priority(11) <Form1_Load>b__3_5 done.
tagName c -> b do[6].priority(13) <Form1_Load>b__3_6 done.
b -> a do[2].priority(7) <Form1_Load>b__3_2 done.
※正規表現での判定EnableRegexp = true
でも使用可能ですが、各種ラッパーと違って定義をそのまま判定に使用します
StateVectorを複数使用する場合
コンストラクタにlistName
を付加します。(下記ではlist_A
)
m_stateVector = new StateVector("list_A", "init", list);
デバッグ出力の先頭にlist_A
が付加されているので、
他のStateVector
定義が混在しても識別が容易になります
list_A: list[0].priority(0) init -> a , InitState
list_A: list[0].priority(1) init -> a , <Form1_Load>b__3_0
list_A: list[0].priority(2) init -> b , InitState
list_A: list[0].priority(3) init -> b , <Form1_Load>b__3_0
list_A: list[0].priority(4) init -> c , InitState
list_A: list[0].priority(5) init -> c , <Form1_Load>b__3_0
list_A: list[1].priority(6) a -> b , <Form1_Load>b__3_1
list_A: list[2].priority(7) b -> a , <Form1_Load>b__3_2
list_A: list[3].priority(8) a -> a , <Form1_Load>b__3_3
list_A: list[4].priority(9) b -> b , <Form1_Load>b__3_4
list_A: list[5].priority(10) a -> c , <Form1_Load>b__3_5
list_A: list[5].priority(11) b -> c , <Form1_Load>b__3_5
list_A:tagName list[6].priority(12) c -> a , <Form1_Load>b__3_6
list_A:tagName list[6].priority(13) c -> b , <Form1_Load>b__3_6
list_A init -> a do[0].priority(0) InitState done.
list_A init -> a do[0].priority(1) <Form1_Load>b__3_0 done.
list_A a -> b do[1].priority(6) <Form1_Load>b__3_1 done.
list_A b -> c do[5].priority(11) <Form1_Load>b__3_5 done.
list_A tagName c -> b do[6].priority(13) <Form1_Load>b__3_6 done.
list_A b -> a do[2].priority(7) <Form1_Load>b__3_2 done.
※1つのForm
にStateVector
は1つか2つ使うくらいが丁度いいと思います。
まとめ
メリット
- 状態遷移に関連する条件分岐を削減し、定義個所をまとめることが出来る
- 仕様変更の際、状態遷移の定義を変更しやすくなるため、バグが減る
- 状態遷移の定義からPlantUML等を使用して図を自動生成可能(今後の課題)
- PlantUML等の定義から状態遷移の定義を自動生成可能(今後の課題)
デメリット
- 状態遷移処理に使用するリソース(メモリ・CPU)が増加するので、頻繁な処理には不向き
- デバッグ時のステップ実行が追いかけにくくなる
- 仕組みを知った上で使わないと混乱する