Prosta implementacja maszyny stanowej
W najprostszej postaci maszynę stanową można zakodować w postaci zagnieżdzonych instrukcji switch-case:
int sm_event_handler( struct * event_t ev ){
switch( state ){
case STATE_OFF:
//obsluga zdarzen w stanie OFF
switch( ev->signal ){
case SIG_ENABLE:
sm_transition( STATE_WAITING );
return 0;
}
return -1;
case STATE_ACTIVATED:
//obsluga zdarzeń w stanie ACTIVATED
switch( ev->signal ){
case SIG_TIMER:
blink();
return 0;
case SIG_MATCH:
beep();
return 0;
...
Czytelność takiego kodu pozostawia wiele do życzenia, podobnie jest z jego efektywnością.
Łatwo zauważyć, że zupełnie niepotrzebnie jest wykonywane wielokrotne sprawdzanie czy dany stan jest taki czy inny (case'y w zewnętrznym switch).
Maszyna stanowa - lepsza implementacja
Problemów obecnych w prostej implementacji można łatwo uniknąć, wystarczy zauważyć, że numery kryjące się pod symbolicznymi nazwami stanów (np. STATE_OFF, STATE_ACTIVATED itd) mogą tworzyć zbiór kolejnych liczb naturalnych.
Nic nie stoi na przeszkodzie aby je zdefiniować następująco:
typedef enum{ STATE_OFF=0, STATE_ACTIVATED, STATE_WAITING, STATE_DETECTED } state_t;
state_t state = STATE_OFF; //zmienna na stan bieżący, zainicjowana stanem STATE_OFF
Dzięki temu możliwe jest rozbicie jednej funkcji obsługi zdarzeń na oddzielne funkcje dla każdego stanu i umieszczenie wskaźników do tych funkcji w tablicy:
typedef int (*sm_handler_t)( struct event_t * ev );//wskaźnik do funkcji
//tablica wskaźników
sm_handler_t handlers[]={ sm_handler_state_off,
sm_handler_state_activated,
sm_handler_state_waiting,
sm_handler_state_detected
};
Zamiast switch-case należy wywoływać funkcję z tablicy, która jest umieszczona na pozycji wskazywanej przez bieżący stan.
int sm_event_handler( struct event_t* ev ){
//wywołanie funkcji:
return handlers[ state ](&ev);
}
//definicja pojedynczego handlera
static int sm_handler_state_off( struct event_t * ev )
{
switch( ev->signal ){
case SIG_ENABLE:
sm_transition( STATE_WAITING );
return 0;
}
return -1;
}
//dalej definicje handlerów dla kolejnych stanow
...
Jak widać osiąga się w ten sposób izolację obsługi zdarzeń dla poszczególnych stanów. Kod jest bardziej czytelny i wywoływana jest tylko ta część, która jest związana z bieżącym stanem.
Więcej maszyn stanowych
Powyższe przykłady implementacji ilustrują jedynie pewną technikę, nie pokazują natomiast jak radzić sobie z:
- wieloma instancjami maszyny tego samego typu,
- definiować maszyny stanowe, różnych typów,
aby zobaczyć pewne rozwiązanie można zajrzeć do implementacji maszyn stanowych w systemie DioneOS.
Definiowanie instancji maszyny stanowej można rozwiązać przez wprowadzenie struktury zawierającej wszystkie zmienne, które były dotychczas globalne:
struct state_machine_t{
sm_handler_t * handlers; //tablica handlerów dla poszczególnych stanów
int state; //bieżący stan
int states_nb; //liczba stanów
void * xdata; //jakieś dodatkowe prywatne dane maszyny stanowej
}
A następnie przekazywać tą strukturę jako argument do funkcji-handlerów i dalej gdzie ważne jest jakiej maszyny dotyczy operacja.
Zamieszczony tu opis dotyczy języka C (jeśli używa się C++ jeszcze się to ułatwi: kolejne instancje to po prostu obiekty danej klasy).
int sm_handler_state_off( struct state_machine_t * sm, struct event_t* ev )
{
switch( ev->signal ){
case SIG_ENABLE:
sm_transition( sm, STATE_WAITING );
return 0;
return -1;
}
Definiowanie różnych typów maszyn
Gdy rozważa się definiowanie różnych typów maszyn stanowych, konieczne jest zaplanowanie odpowiedniego nazewnictwa (przestrzeni nazw).
Trzeba bowiem zdefiniować stany oraz sygnały dla maszyny każdego rodzaju. Można rozważać wprowadzenie globalnego zbioru sygnałów (jakiś enum)
gdzie kolejny nowy typ maszyny dodaje swoje sygnały. Taka implementacja zbiega się z rozwiązaniem, w którym sygnały (tzn. rodzaje zdarzeń)
są niezależne od maszyny stanowej i są zbiorem ogólnym.
Można przyjąć konwencję definiowania maszyny nowego typu w osobnym pliku o nazwie związanej z tworzonym typem (takie postępowanie jest wykorzystywane w innych językach programowania).
Taki plik zawiera cały niezbędny kod do obsługi maszyny stanowej.
Oddzielenie definicji od wykorzystania (framework, biblioteka)
Im więcej razy wykorzystuje się maszyny stanowe, tym bardziej kusi rozwiązanie, w którym użytkownik nie zajmuje się samodzielną implementacją maszyn stanowych a tylko definiuje
specyficzne dla niego elementy albo używa już istniejące typy maszyn. Istnieją rozwiązania (m.in. w systemie DioneOS) gdzie dostarczany jest framework, który tworzy pewnego rodzaju
szablon rozwiązania maszyny stanowej. Może to mieć postać źródeł w języku C, makrodefinicji oraz nagłówków. Programista definiuje maszynę swojego typu z wykorzystaniem tego frameworka
definiując jedynie handlery, zdarzenia i stany. W kolejnym kroku, aby wykorzystać jakiś typ powołuje się instancję takiej maszyny stanowej. Można dostrzec tu kolejne poziomy
abstrakcji: