はじめに
「C言語でトライ! デザインパターン」
今回はStateパターンです、割とCでもよく見ます。StateMachineの形で表現されることが多いですね。
こちらについてはもうすでに素晴らしいOSSがある模様。UMLの状態遷移表まで出力してくれるんですね。凄い!
こちらの深堀をしても面白いのですが地で行きます。理由はコード構成自体もよく使うから。
デザインパターン一覧
作成したライブラリパッケージの説明
公開コードサンプルはこちら, ライブラリパッケージはこちら
2018/5/20 API変更履歴を追加しました。API仕様は変わっていませんが、定義方法や説明を追加しています。
その7. Stateパターン
wikipegiaより抜粋。
State パターン(英: state pattern、ステート・パターン)とは、プログラミングで用いられる振る舞いに関するデザインパターンの一種である。このパターンはオブジェクトの状態(state)を表現するために用いられる。ランタイムでそのタイプを部分的に変化させるオブジェクトを扱うクリーンな手段となる。
状態によって同じ関数、メソッドの呼び方は変えずに振る舞いを切り替えます。
###状態遷移
状態についてはこんな感じの図を一度は見たことがあると思います。
スマホの状態遷移をざっくり書いてみました。起動でSDとかを認識した後はWifiや3Gで接続。
Wifiの場合は切断しちゃうことがあるので切断状態に移行→他のAPを探してなければ3Gへ移行。接続状態へといった感じですかね。
これは凄い単純な図ですが、SD, 無線LAN, 3Gと、この短い説明の中でも状態によって振る舞いが変わる登場人物が出てきます。
これらの処理の中で、状態による分岐をがりがりと書くのは大変な作業…。せめて状態だけでも誰か管理してあげれば⇒Stateパターンの出番というわけです。
###状態遷移表:StateMachine
上記で状態遷移図を書きましたが、あの図ではまだ開発は出来ません。状態が変わるトリガー、イベントがわからないと意味が足りないからです。というわけで上の話に出したイベントを足しました。
これでもいいのですが、各状態の時にどう遷移が起きるのかわかりやすかったりします。そこで使うのが**状態遷移表(StateMachine)**です。上記例だとこんな感じ。
状態\イベント | 電源ON、SD等認識 | Wifi or 3G接続 | Wifiと切断 |
---|---|---|---|
開始 | 起動 | - | - |
起動 | - | 接続中 | - |
接続中 | - | - | 切断中 |
切断中 | - | 接続中 | - |
状態をちゃんと定義していれば、その状態でどんなイベントに対して何をするのかがわかるようになりました。
Stateを考えるうえで、StateMachineは切っても切り離せない関係と言えるかもしれませんね。
ライブラリ
よく見かける実装は関数ポインタによる切り替え。これの何が困るかっていうととにかく見にくい。これを解消したライブラリを、Stateパターン、StateMachine実現の2つ分作成します。
###クラス設計
StateManager, StateMachineそれぞれインスタンスを生成してユーザー側で保持してもらう形式としています。
コード
先に場所だけ。共通で以下に置いてあります。
https://github.com/developer-kikikaikai/design_pattern_for_c/tree/master/state
動作環境: Ubuntu 18.04 Desktop, CentOS 5.1 Desktopで動作確認済み, 大抵のLinux OSなら動くと思います。
ライブラリ1. stateパターンの実現
###クラス設計
stateと関数をペアでライブラリに登録。後はstateを変更する度にcallで呼ばれる関数が変わります。また、表示用関数によりstateと関数の対応表が表示されます。
いい点
- 最初に状態に対応した関数を登録しておけば、状態変更 && APIコールだけで状態に合わせた関数が実行される。
- 後からの状態用関数変更、状態追加にも対応
- 表示関数があるから現状が見える
使いどころ
- 状態ベースのシステムを作る場合
欠点
- 内部で排他をかけていないため、マルチスレッドでの頻繁な状態変更には耐えられません。
※頻繁に色々なスレッドから同時に状態変更をするというシステム構成が思いつかなかったので、排他はなしにしました。
ライブラリ1詳細
API定義
//stateと対応関数を設定する構造体
typedef struct state_info_t {
int state;
char *name;/*! 名前はマクロを使って勝手に設定します。*/
int (*state_method)(void *arg);/*! state method called at state_manager_call */
} state_info_t;
//初期化時はこちらを使用。
#define STATE_MNG_SET_INFO_INIT(instate, fname) {.state=(instate), .name=#fname, .state_method = (fname)}
//途中で値を更新する場合はこちらを使用。
#define STATE_MNG_SET_INFO(info, instate, fname) {(info).state=(instate); (info).name=#fname; (info).state_method = (fname) ; }
struct state_manager_t;
typedef struct state_manager_t *StateManager;
//new時にstate_info_tをリストで渡します。update_methodで途中の追加、更新もOK
StateManager state_manager_new(size_t state_info_num, const state_info_t * state);
int state_manager_update_method(StateManager this, const state_info_t * state);
//set_stateで状態変更します。今の状態はget_current_stateで取得できます。
void state_manager_set_state(StateManager this, int state);
int state_manager_get_current_state(StateManager this);
//登録関数はこちらで実行。今の状態に合わせた関数が呼ばれます。
int state_manager_call(StateManager this, void *arg);
//登録関数がよくわからん!って時はこちらで現状の確認が出来ます。
void state_manager_show(StateManager this);
void state_manager_free(StateManager this);
使い方:
-
state_manager_new
もしくはその後のstate_manager_update_method
でstate_info_tを登録。 -
state_manager_set_state
で状態変更。 -
state_manager_call
で今の状態に対応した登録関数を実行。 - 最後に
state_manager_free
で解放。
設定内容確認のため、state_info_t
に関数名name
を設定するようにしています。その為、name設定用のマクロを用意しています。
サンプル
テストコードから抜粋。一部改変してます。初期化はこのようにSTATE_MNG_SET_INFO_INITマクロを利用します。
ここではSTATE_BEGIN⇒test_state_begin, STATE_TESTING⇒test_state_testing, STATE_END⇒test_state_testingという3つの状態に対してそれぞれ関数を設定。
後はstate_manager_new
でクラスのインスタンス作成、state_manager_set_state
で状態設定後state_manager_call
を呼ぶだけです。
//初期化
state_info_t state_info[] = {
//set state and function by using Macro
STATE_MNG_SET_INFO_INIT(STATE_BEGIN, test_state_begin),
STATE_MNG_SET_INFO_INIT(STATE_TESTING, test_state_testing),
STATE_MNG_SET_INFO_INIT(STATE_END, test_state_end),
};
//インスタンスを作成
StateManager manager = state_manager_new(sizeof(state_info)/sizeof(state_info[0]), state_info);
if(!manager) {
printf("####Failed to call state_manager_new\n");
return -1;
}
int response=0;
//状態を変えて
state_manager_set_state(manager, STATE_BEGIN);
//call。ここではtest_state_beginが呼ばれます。
state_manager_call(manager,&response);
もしstate_info_tの値を初期時以外で設定する場合は、STATE_MNG_SET_INFO
を使います。
state_info_t state_info;
STATE_MNG_SET_INFO(state_info, STATE_TESTING, test_state_testing);
state_manager_show
実行時はこのような見え方になります。
-------- Show state table --------
[Current state: 2] [method: test_state_end]
[state: 0] [method: test_state_begin]
[state: 1] [method: test_state_testing]
[state: 2] [method: test_state_end]
----------------------------------
ただし、関数ポインタ変数経由でSTATE_MNG_SET_INFO
すると、流石にポインタ先の関数名はわからないためこんな感じになります。
-------- Show state table --------
[Current state: 2] [method: state_info_data[i].state_method]
[state: 0] [method: state_info_data[i].state_method]
[state: 1] [method: test_state_testing]
[state: 2] [method: state_info_data[i].state_method]
----------------------------------
ライブラリ2. 状態遷移表:StateMachineの実現
概要
eventのIDとstate_info_tリストをペアでライブラリに登録。後はstateの変更とcall時に指定したeventのIDで呼ばれる関数が変わります。また、表示用関数によりeventと対応したstateと関数の対応表が表示されます。
いい点
- 状態遷移表に登場する状態遷移用の関数を一通り登録しておけば、状態変更 && APIコール時のevent指定だけで状態に合わせた関数が実行される。
- 後からの状態用関数変更、状態追加にも対応
- 表示関数があるから現状が見える
- 状態遷移表の関数を実行するスレッドを新しく作成するかが選択可能
使いどころ
- 状態遷移表ベースのシステムを作る場合
欠点
- 内部で排他をかけていないため、マルチスレッドでの頻繁な状態変更には耐えられません。
※頻繁に色々なスレッドから同時に状態変更をするというシステム構成が思いつかなかったので、排他はなしにしました。
ライブラリ2詳細
API定義
#include "state_manager.h"
#include "event_threadpool.h"
//eventと対応するstate_info_tリストを設定する構造体
typedef struct state_event_info_t {
int event;
size_t state_num;
state_info_t *state_infos;
} state_event_info_t;
struct state_machine_t;
typedef struct state_machine_t *StateMachine;
//event_threadpoolを利用出来るようにしました。
typedef struct state_machine_info {
StateMachine state_machine;
int thread_num;
} state_machine_info_t, *StateMachineInfo;
//new時にstate_event_info_tをリストで渡します。update_methodで途中の追加、更新もOK。また、is_multithreadで関数実行スレッドを別にすることも出来ます。
StateMachineInfo state_machine_new(size_t event_num, const state_event_info_t * event_infos, int is_multithread);
int state_machine_update_machine(StateMachine this, const state_event_info_t * event_info);
//set_stateで状態変更します。
void state_machine_set_state(StateMachine this, int state);
//登録関数はこちらで実行。指定したeventに対応した、今の状態に合わせた関数が呼ばれます。もしis_multithread=1でnewした場合はresponseをコールバックで取得
int state_machine_call_event(StateMachine this, int event, void *arg, void (*response)(int result));
//登録関数がよくわからん!って時はこちらで現状の確認が出来ます。
void state_machine_show(StateMachine this);
void state_machine_free(StateMachineInfo this);
#include "state_manager.h"
#include "event_threadpool.h"
//eventと対応するstate_info_tリストを設定する構造体
typedef struct state_event_info_t {
int event;/*!< event event id */
size_t state_num;/*!< state num*/
state_info_t *state_infos;/*!< state list, please see state_manager.h defition*/
} state_event_info_t;
//event_threadpoolを利用出来るようにしました。
typedef struct state_machine_t *StateMachine;
typedef struct state_machine_info {
StateMachine state_machine;
int thread_num;
} state_machine_info_t, *StateMachineInfo;
/**
* @brief Create StateMachineInfo class
* @param[in] event_num event size
* @param[in] event_infos list of event state data
* @param[in] threadpool event threadpool instance by creating event_threadpool API if you want to use state machine in other threads
* @retval !=NULL this class handle
* @retval NULL error
*/
StateMachineInfo state_machine_new(size_t event_num, const state_event_info_t * event_infos, EventTPoolManager threadpool );
/**
* @brief update sate
*
* @param[in] this StateMachineInfo class instance returned at state_machine_new
* @param[in] event_info update list of event state data.
* @retval STATE_MNG_SUCCESS success
* @retval other failed
*/
int state_machine_update_machine(StateMachineInfo this, const state_event_info_t * event_info);
/**
* @brief set state
*
* @param[in] this StateMachineInfo class instance returned at state_machine_new
* @param[in] state update state, if there is no state in set list, state is changed to latest order.
* @return none
*/
void state_machine_set_state(StateMachineInfo this, int state);
/**
* @brief get state
*
* @param[in] this StateMachineInfo class instance returned at state_machine_new
* @return state
*/
int state_machine_get_current_state(StateMachineInfo this);
/**
* @brief call event trigger
*
* @param[in] this StateMachineInfo class instance returned at state_machine_new
* @param[in] event event id related to this function
* @param[in] arg event argument
* @param[in] arglen event argument len
* @param[in] response response callback method. If you set is_multithread=true , you must set this response callback,
* @retval return_value of method if you set by single thread mode
* @retval STATE_MNG_SUCCESS and result is in callback you set callback if you set by multi thread mode.
*/
int state_machine_call_event(StateMachineInfo this, int event, void *arg, int arglen, void (*response)(int result));
/**
* @brief set state
*
* @param[in] this StateMachine class instance returned at state_machine_new
* @return none
*/
void state_machine_show(StateMachineInfo this);
/**
* @brief free StateMachine class
*
* @param[in] this StateMachine class instance returned at state_machine_new
* @return none
*/
void state_machine_free(StateMachineInfo this);
#endif
使い方:
-
state_machine_new
もしくはその後のstate_machine_update_machine
でstate_event_info_tを登録。 -
state_machine_set_state
で状態変更。 -
state_machine_call_event
で、指定したeventと今の状態に対応した登録関数を実行。 - 最後に
state_machine_free
で解放。
サンプル
テストコードから抜粋。一部改変してます。StateManagerの時と使い方は大体同じ。
イベントごとにstate_info_t を用意して、state_event_info_t のリストを作成。
後はstate_machine_new
でクラスのインスタンス作成、state_machine_set_state
で状態設定後state_machine_call_event
を呼ぶだけです。
//イベントEVENT_BEGIN用のstate_info_tを作成
state_info_t state_for_beginevent[] = {
STATE_MNG_SET_INFO_INIT(STATE_BEGIN, test_state_begin_event_begin),
STATE_MNG_SET_INFO_INIT(STATE_END, test_state_end_event_begin),
};
//イベントEVENT_END用のstate_info_tを作成
state_info_t state_for_endevent[] = {
STATE_MNG_SET_INFO_INIT(STATE_BEGIN, test_state_begin_event_end),
STATE_MNG_SET_INFO_INIT(STATE_END, test_state_end_event_end),
};
//ここでstate_event_info_t リストを作成
state_event_info_t event_info[] = {
{EVENT_BEGIN, sizeof(state_for_beginevent)/sizeof(state_for_beginevent[0]), state_for_beginevent},
{EVENT_END, sizeof(state_for_endevent)/sizeof(state_for_endevent[0]), state_for_endevent},
};
//後はinstanceを作成して
StateMachine state_machine = state_machine_new(sizeof(event_info)/sizeof(event_info[0]), event_info, 0);
if(!state_machine) {
printf("####Failed to call state_machine_new\n");
return -1;
}
//状態を設定
state_machine_set_state(state_machine, STATE_BEGIN);
//eventを指定してcall。ここではtest_state_begin_event_beginが呼ばれます。
state_machine_call_event(state_machine, EVENT_BEGIN, NULL, NULL);
state_machine_show
実行時はこのような見え方になります。
===[event: 1]===
-------- Show state table --------
[Current state: 1] [method: test_state_end_event_end]
[state: 0] [method: test_state_begin_event_end]
[state: 1] [method: test_state_end_event_end]
----------------------------------
感想
よく見るStateMachine、不満を2つ持っていました。
- 関数ポインタの配列で表現され、好きな人は使いまくるから本当に追いにくい時がある。
- 時間のかかる処理が絡むことがあるので、結構StateMachine処理用にスレッドを立てたくなることがある。
今回のライブラリは、この2つの不満を解消して、設定の仕方も比較的見やすく出来たと思うので、する為に作ったようなものなので、不満が解消できる形になってかなり満足してます。
Observerパターンとの相性も良さそうなので、組み合わせれば結構でかいシステムも楽に作れるんじゃないかなと思います。
API変更履歴
2018/05/20 APIに対するクラス図がおかしかったので修正。APIのクラス名を修正(XXXClassのClassを削除)。コードのURLを変更
2018/06/07 StateMachineのマルチスレッド利用ケースをスレッドプールを利用する形に変え、StateMachineのスレッドと同一スレッドで処理が実行可能なように修正
参考
素晴らしいOSSを紹介されていた方の記事
要約:知らないと損するアプリ開発におけるStateMachineの活用法
State パターンの説明
http://www.techscore.com/tech/DesignPattern/State.html/
ステートマシンのわかりやすい説明
ステートマシン図& 状態遷移表 チュートリアル