2017年7月16日 星期日

About states, part III

In last blog I talked about how state pattern is not enough for real application development if the states are quite complex. Even if you finished the implementation you would put in a lot of effort into maintenance. So, what can you do?

We talked about state transition and hierarchical and parallel states. For better understanding, you should read a paper published over 30 years ago, David Harel, statecharts. You should read it at least once. Read it, and come back here, I will be waiting here.

Long time ago, I was so frustrating on how difficult it is to manage states in program until I found this paper via google about 10 years ago. I read it and think to myself, that's it! If there's a library offer the statecharts functionality, it will be great! It certainly will enable us to develop better software and make it much easier to maintain.

To my surprise, I couldn't find a C++ library offer this functionality. As a result, I wrote one myself. Till these days, it's still one of the best things I ever developed.

Although it's most intuitive as human beings are good at grasping graphical charts, it is not so for a computer. You have to translate it to texts so computer can process it. There's a specification developed by W3C named SCXML in XML format. Please familiarize yourself with them from the official side.

    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>

My idea is to use the format defined in scxml except the executable and control flow part, since we are using it with a programming language. It would be just like a black box. By using the signal/slots mechanism you simply connect to the signals it triggered when the onentry, onexit, or ontransit happened. You basically only invoked it's enqueEvent() and frame_move() methods. In stead of make many function calls, It's much easier to use by making it a framework, avoiding handling the trivia. Although you will have to do thing following it's rule.

Well, later I found out that the Boost libraries offer not only one but two libraries that support state machines: boost Meta State Machine and boost statechart. They use jargon defined in UML, there's difference with what Harel or SCXML used. I personally don't like it. You could try them out to see if you like either one, maybe you will find one suit your need. It seems they were not present when I start looking for a state machine solution. But even if they were, I would still develop one myself though, they are simply not what I wanted. If you are writing your own version, consider follow the scxml format like me. Although something like JSON may be more to your liking, but I think it's a good thing to have a standard.

Code utilize my library looks like this:
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;
    

Observe that you can define the statcharts as a string in one place. In stead of spread the states information all over the place and define the states relationship all over the place. It is much easier to understand and later to maintain.

Below is an scxml definition for one of a game component I developed previously.

<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>

Intimidating, right? If it's not that I utilized the statechart library. I can't image how difficult it would be to debug or trace code if I have to found all the states information all over the place.

I will later upload this scm library to github, you will find it here, stay tuned.

沒有留言:

張貼留言

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...