我們談到了狀態的轉換(transition)還有階層式與平行狀態等,這裏有篇David Harel, statecharts在數十年前寫的paper,你一定要讀一次,去讀完它,我等你。
很久很久以前我對於怎麼處理程式中狀態的問題非常傷腦筋,直到我大概十年前google到了這篇paper,讀完後覺得豁然開朗,心想如果有提供這種功能的library可以用就太好了,如果我們能善加利用支援statechart功能的library,一定能寫出更好使用與維護的程式碼。
讓我疑惑的是,似乎找不到C++的library,結果我自己寫了一套。直到今天還是我開發的東西中最好用的東西之一。
用圖型表示雖然最直覺,人類能很快懂,但是要使用在電腦程式裏當然還是得變成文字才方便。W3C定義了SCXML, 用xml的格式來表達。
重點的這些元件還請上官網去熟悉一下
3.2 <scxml>
3.3 <state>
3.4 <parallel>
3.5 <transition>
3.6 <initial>
3.7 <final>
3.8 <onentry>
3.9 <onexit>
3.10 <history>
我的構想就是使用跟scxml定義的一樣的格式但是不實作它定的executable的功能,把它當一個黑盒子一樣,它用signal/slot的機制讓你去connect onentry, ontransit, onexit等時候發出的signal。控制它基本上只會用到enqueEvent()與frame_move()。用一般的方式只是一些function叫來叫去的話不是很好用,我們需要framework來幫我們處理很多鎖事,儘管使用的話就得遵守framework的規則。
後來我發現boost library中有兩個library提供state machine的功能,boost Meta State Machine與boost statechart。不過它們使用的術語是UML中定義的,與原本Harel使用的不同,個人實在不欣賞。你可以學著使用看看,或許合你味口也說不定。當初似乎還沒這兩個library,不過即使是現在我應該也會選擇--自己寫一個,建議如果你也想寫一套的話,照WC3的spec來開發,有一個共同的標準是好事。
我個人寫的scm的framework, 使用起來像這樣
static string app_scxml = "<scxml initial='init'>" " <state id='init'>" " <transition event='login' target='online'/>" " </state>" " <state id='online'>" " <state id='idle'>" " <transition event='hmi-connected' target='use'/>" " </state>" " <state id='use'>" " <transition event='hmi-disconnected' cond='condNoConnection' target='idle'/>" " </state>" " </state>" " <transition event='logout' target='offline'/>" " <final id='offline'/>" "</scxml>" ; struct AppServiceHandler {
// member declarations
.
.
.
wtk::Net::StateMachine *mach_;
AppServiceHandler()
: (0)
.
.
.
{ mach_ = StateMachineManager::instance()->getMach("app_scxml"); REGISTER_STATE_SLOT (mach_, "init", &AppServiceHandler::onentry_init, &AppServiceHandler::onexit_init, this); REGISTER_STATE_SLOT (mach_, "online", &AppServiceHandler::onentry_online, &AppServiceHandler::onexit_online, this); REGISTER_STATE_SLOT (mach_, "idle", &AppServiceHandler::onentry_idle, &AppServiceHandler::onexit_idle, this); REGISTER_STATE_SLOT (mach_, "use", &AppServiceHandler::onentry_use, &AppServiceHandler::onexit_use, this); REGISTER_STATE_SLOT (mach_, "offline", &AppServiceHandler::onentry_offline, &AppServiceHandler::onexit_offline, this); boost::function<bool()> cond_slot; mach_->setCondSlot("condNoConnection", cond_slot = boost::bind(&AppServiceHandler::PRIVATE::condNoConnection, this)); mach_->StartEngine(); } ~AppServiceHandler() { mach_->ShutDownEngine(true); } // member functions
.
.
. // void onentry_init (); void onexit_init (); void onentry_online (); void onexit_online (); void onentry_idle (); void onexit_idle (); void onentry_use (); void onexit_use (); void onentry_offline (); void onexit_offline (); bool condNoConnection () const;
注意到我們直接用scxml的格式把state machine定義好,相對於此,你在boost chart或一些其它的library中,各個state的定義與關係的建立通常散落在各處程式碼中。那樣實在不好理解與維護。
下面是我以前開發的一個遊戲中角色的scxml
<scxml initial="alive"> <state id="spawn"> <transition event="play" target="alive"/> </state> <parallel id="alive"> <transition event="die" target="dead"/> <state id="gun"> <transition event="gun" target="gun_on"/> <state id="gun_off"> </state> <state id="gun_on"> <state id="gun_reload"> <transition event="gun_reloaded" target="gun_fire"/> </state> <state id="gun_fire"> <transition event="no_bullet" target="gun_off"/> </state> </state> </state> <state id="wave"> <transition event="wave" target="wave_on"/> <state id="wave_off"> </state> <state id="wave_on"> <transition event="no_wave" target="wave_off"/> </state> </state> <state id="radar"> <transition event="radar" target="radar_on"/> <state id="radar_off"> </state> <state id="radar_on"> <transition event="radar_off" target="radar_off"/> </state> </state> <state id="shield"> <state id="shield_off"> <transition event="shield" target="shield_on"/> </state> <state id="shield_on"> <transition event="collide_dot" target="shield_explode"/> <transition event="collide_boss" target="shield_explode"/> </state> <state id="shield_explode"> <transition event="shield" target="shield_on"/> <transition event="shield_off" target="shield_off"/> </state> </state> <state id="phantom"> <transition event="phantom" target="phantom_on"/> <state id="phantom_off"> </state> <state id="phantom_on"> <transition event="phantom_off" target="phantom_off"/> </state> </state> <state id="explode"> <transition event="explode" cond="cond_FB_enabled" ontransit="spawnExplode" target="firebird_on"/> <transition event="explode" ontransit="spawnExplode" target="explode_on"/> <state id="explode_off"> </state> <state id="explode_on"> <transition event="explode_off" target="explode_off"/> </state> <state id="firebird_on"> <transition event="firebird_off" target="explode_off"/> </state> </state> <state id="display"> <transition event="shielding_on" cond="!In(firebirding)" target="shielding"/> <transition event="phantom_on" cond="!In(firebirding)" target="phantoming"/> <transition event="firebird_on" cond="!In(firebirding)" target="firebirding"/> <state id="normal"> </state> <state id="shielding"> <transition event="shielding_off" cond="In(phantom_on)" target="phantoming"/> <transition event="shielding_off" target="normal"/> </state> <state id="phantoming"> <transition event="phantom_off" cond="In(shield_explode)" target="shielding"/> <transition event="phantom_off" target="normal"/> </state> <state id="firebirding"> <transition event="firebird_off" cond="In(phantom_on)" target="phantoming"/> <transition event="firebird_off" cond="In(shield_explode)" target="shielding"/> <transition event="firebird_off" target="normal"/> </state> </state> </parallel> <state id="dead"> <transition event="play" target="alive"/> </state> </scxml>
有沒有覺得很可怕?如果不是使用這一套statechart的方式,而是讓狀態的資訊散落在程式中,不敢想像維護起來要花多大的功夫。
稍後我將上傳我寫作的scm library到github, 你可以由這裏取得。
沒有留言:
張貼留言