2017年7月16日 星期日

關於狀態(states), part III

上次我們談到使用state pattern的支巧不足以對付實際開發的需求,只是套用state pattern的方法在維護上將付出很大的代價,所以該如何是好呢?

我們談到了狀態的轉換(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 Machineboost 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, 你可以由這裏取得。

沒有留言:

張貼留言

ftps.space released!

  I started doing web site development some time ago.   After fiddled with Flask, nginx, javascript, html, css, websocket, etc, etc... I h...