Kuten sarjan ensimmäisessä osassa lupasin, esittelen tässä osassa kaksi hieman toisistaan poikkeavaa lähestymistapaa siihen, miten tilakone voidaan käytännössä toteuttaa ohjelmointikielellä. 

Ensimmäiseksi tarkastelemme melko suoraviivaista tapaa toteuttaa tilakone. Oletamme, että kielenä on Javan tai C#:n tapainen olio-ohjelmointikieli. Lähestymistavassa tilat ovat luokkia ja tapahtumat niiden metodeja. Esimerkkinä käytämme 1. osassa esiteltyä suljetun tilan valvontajärjestelmää. Alla on listattu osa tilojen yhteisen kantaluokan sekä kahden esimerkkitilan toteutuksesta:

public abstract class State
{
    protected IStateMachine _sm;


    protected State(IStateMachine sm)
    {
        _sm = sm;
    }


    public virtual void Entry()
    {
        // Log entry
    }

    public virtual void Exit()
    {
        // Log exit
    }


    public virtual void OnPinEntered(int pinNumber)
    {   
    }

    public virtual void OnMotionDetection()
    {   
    }

    public virtual void OnSwitchOff()
    {        
    }
}


public class IdleState : State
{
    ...

    public override void OnPinEntered(int pinNumber)
    {
        if (IsValid(pinNumber))
        {
            _sm.ChangeState(new AuthorizedState(_sm));
        }
    }


    public override void OnMotionDetection()
    {
        _sm.ChangeState(new AlarmState(_sm));
    }
}


public class AlarmState : State
{
    ...


    public override void Entry()
    {
        base.Entry();
        _sm.SendAlarm();
    }


    public override void OnSwitchOff()
    {
        _sm.ChangeState(new OffState(_sm));
    }
}


Seuraavassa osa itse tilakoneluokasta:

public class StateMachine : IStateMachine
{
    private State _state;

    ...
    
    public StateMachine()
    {
        _state = new IdleState(this);
        ...
    }


    public void OnPinEntered(int pinNumber)
    {
        lock (_internalLock)
        {
            _state.OnPinEntered(pinNumber);
        }
    }


    ...


    public void ChangeState(State state)
    {
        lock (_internalLock)
        {
            _state.Exit();
            _state = state;
            _state.Entry();
        }
    }


    public void SendAlarm()
    {
        // Send alarm message to channel
    }

     ...
}

Kuten yllä mainittiin, tilakoneen eri tilat on toteutettu luokkina. Tilakoneessa on tallessa olio, joka edustaa nykytilaa vastaavaa luokkaa. Kun tilakoneeseen tulee ulkopuolelta jokin tapahtuma (esimerkkinä OnMotionDetection), kutsu ohjataan sillä hetkellä päällä olevaan tilaan. Tilat periytyvät samasta abstraktista kantaluokasta, jonka rajapinnassa on kaikki eri tapahtumia vastaavat metodikutsut. Näillä metodeilla voi olla oletustoteutukset, jotka tyypillisesti eivät tee mitään (paitsi tulostavat tiedon kutsusta lokiin). Eri tilat (aliluokat) toteuttavat nämä metodit eri tavalla ja reagoivat siksi eri tavoin tapahtumiin. Tapahtumat, joista tietty tila ei ole kiinnostunut, se jättää toteuttamatta, joten kyseisessä tilassa ei reagoida niihin. Kyseessä on siis esimerkki polymorfismista: sama metodikutsu delegoidaan ajonaikaisesti eri toteutuksiin.

blogi-tilakone

Tietyssä tilassa määritelty metodi voi tehdä päätöksen siirtyä toiseen tilaan. Tilasiirtymästä voidaan tehdä ehdollinen testaamalla jotakin totuusarvoa ennen siirtymää, kuten esimerkissä ennen siirtymistä Idle-tilasta Authorized-tilaan. Ehdot voivat lukea myös tilakoneen kvantitatiivisia tilamuuttujia. Uuteen tilaan siirryttäessä luodaan uusi olio ko. tilaa edustavasta luokasta, ja se annetaan parametriksi tilakoneen metodille ChangeState. Tämä asettaa luodun olion nykyiseksi tilaksi. Lisäksi kutsutaan vanhan tilan Exit– ja uuden tilan Entry-metodia. Näissä voidaan asettaa muuttujien arvoja tai kutsua jotakin ulkoista metodia, kun tietty toimenpide tarvitaan aina kun tämän tyyppiseen tilaan tullaan tai kun siitä poistutaan. Esimerkiksi saavuttaessa Alarm-tilaan lähetetään hälytysviesti valvomoon.

Toisenlaisetkin totetusyksityiskohdat ovat mahdollisia, esimerkiksi tilasiirtymät voitaisiin määritellä taulukkoon, josta katsotaan mihin tilaan nykyisestä tilasta pitää siirtyä tietyllä tapahtumalla. Yllä oleva metodien uudelleenmäärittelemiseen perustuva ratkaisu kokoaa kuitenkin selkeästi tiettyyn tilaan liittyvän toiminallisuuden samaan paikkaan ja mahdollistaa helpon ehtojen käytön.

Tilakoneluokan lukitus huolehtii, että vain yksi säie kerrallaan pääsee kutsumaan tilakoneen metodeja ja muuttamaan tilaa.

Toisinaan haluamme jakaa tilakoneen tilat hierarkisesti ryhmiin, jotka jakavat keskenään samanlaista käyttäymistä. Esimerkissämme huomaamme, että siinä on monta tilasiirtymää, joissa Switch off -tapahtumalla siirrytään Off-tilaan. Voisimmekin ryhmitellä Idle-, Authorized– ja Alarm-tilat yhden ”On” -koostetilan sisään, joka sisältää yhteisen Switch off -tilasiirtymän. Vastaavasti Alarm-tilan sisälle voitaisiin luoda uusia alitiloja, jos siihen haluttaisiin lisätä uusia toimintoja, kuten vaikkapa hälytyksen kuittaus. Edellä kuvattu tilakoneen toteutusmalli mahdollistaa yksinkertaisten hierarkisten tilakoneiden toteuttamisen käyttämällä useamman tason periytymistä. Ylemmän tason koostetilaa esittää luokka, jossa voidaan määritellä osa metodeista. Toisaalta tästä luokasta periytetyt tilat uudelleenmäärittelevät osan metodeista. Näin alitilat voivat reagoida samalla tavalla johonkin ulkoiseen tapahtumaan X, mutta reagoida eri tavalla toiseen tapahtumaan Y.

Tällä tavalla toteutettu hierarkisen tilakoneen malli on yksinkertaisempi kuin esim. UML-standardissa kuvattu, koska saman koostetilan alitiloja ei automaattisesti yhdistä muu kuin samat tapahtumien käsittelymetodit. Erityisesti saavuttaessa alitilaan koosteen ulkopuolelta tai poistuttaessa ulkopuolelle ei automaattisesti kutsuta koostetilan Entry/Exit-metoja. Tätä voidaan kyllä joissakin tapauksissa simuloida siten, että alitila kutsuu erikseen kantatilan metodeja. Emme myöskään mallinna ns. “historiatiloja”, jotka mahdollistavat koostetilaan palatessa siirtymisen siihen alitilaan, johon viimeksi jäätiin.

Edellä kuvattua tilakoneen toteutustapaa voidaan kutsua vaikkapa tapahtumaohjatuksi malliksi. Tapahtumaohjattu tilakoneen toteutus voi olla suoraviivaisin ratkaisu esim. tilanteessa, jossa kaikki tapahtumat koneeseen tulevat sarjallisesti samasta säikeestä.

Käytäntö kuitenkin osoittaa, että kun tapahtumia tulee rinnakkain useista eri säikeistä, tämä malli ei toimi aina yhtä hyvin, etenkään jos syötteiden ja tilojen määrä kasvaa suureksi. Syitä tähän on monentyyppisiä, eivätkä ne ole aina kovin helppoja hahmottaa. Käsittelemme näitä ongelmia lisää hieman myöhemmin.

Ensin esittelemme kuitenkin tilakoneen toteuttamiseen hieman toisenlaisen lähestymistavan, joka saattaa helpottaa joitakin edellä kuvattuun malliin liittyvistä ongelmista.

Tilaa lukeva tilakone

Siinä missä ulkoa päin tulevat tapahtumat muuttivat suoraan tapahtumapohjaisen tilakoneen tiloja, uudessa versiossa kone itse lukee ulkomaailman tilaa ja päättää tilasiirtymistä. Tätä kutsumme jatkossa “tilaa lukevaksi” tai “pollaavaksi” malliksi. Tämäntyyppinen tilakone ei lähtökohtaisesti reagoi aina tapahtuman tullessa, eli kun ulkoinen tila muuttuu, vaan se tarkkailee vallitsevaa tilaa tiettyinä ajanhetkinä.

Alla on listattu osa geneerisestä tilakoneen toteutuksesta tällä lähestymistavalla – yksityiskohtia on taas jätetty pois. Tässäkin eri tilat on toteutettu eri luokkina. Tilakoneelle annetaan parametreina kaksi rajapintaa: tyyppi TQ on rajapinta jonka kautta kone lukee ympäristön tilaa, TC on rajapinta jonka kautta kone aiheuttaa muutoksia (jako kahteen rajapintaan on sinänsä lähinnä makuasia). Lisäksi tilakoneelle täytyy käynnistää oma säie, joka kutsuu tilakoneen Poll-metodia tietyin väliajoin.

public class PollingStateMachine<TQ, TC>
{
    public PollingState<TQ, TC> State;

    public TQ StatusQuery;
    public TC Control;

    ...

    public void ChangeState(PollingState<TQ, TC> state)
    {
        State.Exit();
        State = state;
        State.Entry();
    }

    public void Poll()
    {
        State.Poll();
    }
}

Alla on esitetty yksi tilakoneen tila, parametrisoituna joillakin esimerkkirajapinnoilla. Tilakone kutsuu joka kierroksella voimassa olevan tilan Poll-metodia, ja tämä kutsu on toteutettu kussakin tilassa siten, että se lukee tällä hetkellä tarpeelliset tiedot ympäristöstä ja päättää niiden perusteella mahdollisesta seuraavasta tilasiirtymästä. Esimerkin Idle-tilassa liikehälytys aiheuttaa siirtymisen Alarm-tilaan, koodin syöttäminen taas Authorized-tilaan. Alarm-tilan Entry-funktiossa taas kutsuttaisiin valvomon hälytystä kuten aiemminkin jne.

public class IdleState : PollingState<IExampleQueryInterface, IExampleControlInterface>
{
    ...

    public override void Poll()
    {
        if (_sm.StatusQuery.MotionDetection())
        {
            _sm.ChangeState(new AlarmState(_sm));
        }
        else if (IsValid(_sm.StatusQuery.EnteredPin()))
        {
            _sm.ChangeState(new AuthorizedState(_sm));
        }
    }
}

Miten tämä tilakonemalli sitten eroaa aiemmin kuvatusta tapahtumapohjaisesta ”standardimallista”? Nopeasti katsottuna ei ehkä paljoakaan, koska samankaltaiset elementit on löydettävissä molemmista. Käytännössä nämä lähestymistavat edustavat kuitenkin hieman erilaista suunnittelufilosofiaa ja ohjaavat järjestelmän toteutusta helposti erilaisille urille. Kirjoituksen viimeisessä osassa tulemme käsittelemään tarkemmin näitä eroja.