MachineWzór.14 W poprzednim wpisie omówiliśmy wzorzec projektowy stan. Wracamy do niego i przy okazji omówimy działania maszyny stanów. Poprzednim razie stworzyliśmy prymitywna maszynę stanów telefonu.

Pytanie, jak to można zrobić korzystając z gotowych rozwiązań jak np. z paczki statless

statles mach7ine.PNG

Zobaczmy jak szybko możemy napisać podobny przykład z telefonem korzystając z tej biblioteki.

state machnies.png

Mam do dyspozycji następujące typy wyliczeniowe z poprzedniego przykladu:

public enum PhoneState
{
    OffHook,
    Connecting,
    Connected,
    OnHook
}

public enum TriggerPhone
{
    CallDialed,
    HungUp,
    CallConnected,
    PlacedOnHook,
    TakenOffHold,
    LeftMessage
}

Teraz użyjmy klasy StateMachine<T1,T2> z tej biblioteki. Jak widzisz mam tutaj do dyspozycji interfejs Fluid. Konfigurujemy jaki wyzwalacz robi jaki stan i możemy uruchomić naszą maszynę.

var call = new StateMachine<PhoneState, TriggerPhone>
    (PhoneState.OffHook);

call.Configure(PhoneState.OffHook)
    .Permit(TriggerPhone.CallDialed, PhoneState.Connecting);

call.Configure(PhoneState.Connecting)
    .Permit(TriggerPhone.PlacedOnHook, PhoneState.OffHook)
    .Permit(TriggerPhone.CallConnected, PhoneState.Connected);

call.Configure(PhoneState.Connected)
    .Permit(TriggerPhone.LeftMessage, PhoneState.Connected)
    .Permit(TriggerPhone.PlacedOnHook, PhoneState.OffHook)
    .Permit(TriggerPhone.TakenOffHold, PhoneState.OnHook);

call.Fire(TriggerPhone.CallDialed);

Teraz porozmawiajmy o samej bibliotece.

W Statless wyzwalacze i stany mogą być dowolnym obiektem lub wartością. Nie muszą to być typy wyliczeniowe. Wracają do przykładu z piwem moglibyśmy skorzystać  z typu logicznego bool by określić czy piwo jest wypite, czy nie.

Mimo wszystko jednak potrzebujemy informacji o wyzwalaczach piwa

public enum BeerTrigger
{
    Drink,
    Fill
}

Co możemy zrobić z tą biblioteką? Po pierwsze nasza maszyna ma swoje zdarzenia. Możemy więc wykonać określoną akcję przy wejściu w dany stan, jak i z jego wychodzenia.

Reprezentują to metody OnEntry i OnExit. Postanowiłem do nich dodać zegarek, który zmierzy ile milisekund minęło od wypicia piwa.  Jak widzisz zastosowanie tych zdarzeń istnieje.

var beerMachine = new
    StateMachine<bool, BeerTrigger>(true);

System.Diagnostics.Stopwatch watch =
    System.Diagnostics.Stopwatch.StartNew();

beerMachine.Configure(false)
    .Permit(BeerTrigger.Fill, true)
    .Ignore(BeerTrigger.Drink);

beerMachine.Configure(true)
    .Permit(BeerTrigger.Drink, false)
    .Ignore(BeerTrigger.Fill)
    .OnEntry(() => watch = System.Diagnostics.Stopwatch.StartNew())
    .OnExit(() =>
        {
            watch.Stop();
            Console.WriteLine("Jak szybko pije");
            Console.WriteLine(watch.ElapsedMilliseconds);
        }
    );

Mamy też metodę Ignore(), która mówi maszynie by ignorować próby wywołania wyzwalacza, gdy jesteśmy w obecnym stanie. Czyli unikniemy dzięki temu wypicia pustego piwa oraz nalania do pełnego piwa. 

Bez metody Ignore() stateless wyrzuci wyjątek InvalidOperationException.

beerMachine.Fire(BeerTrigger.Drink);
beerMachine.Fire(BeerTrigger.Fill);
beerMachine.Fire(BeerTrigger.Fill);
Thread.Sleep(3000);
beerMachine.Fire(BeerTrigger.Drink);
Console.Read();

Oto wynik działania takiego kodu:

działanie aplikacji przy użyciu statless

Sprawdzanie ponownego wejścia

Istnieje też kolejny sposób na przechwycenie ponownego wejścia do danego stanu. Możemy nasz przykład z piwem skonfigurować tak.

var beerMachine2 = new
StateMachine<bool, BeerTrigger>(true);

beerMachine2.Configure(false)
    .Permit(BeerTrigger.Fill, true)
    .OnEntry(transition=>
    {
        if (transition.IsReentry)
            Console.WriteLine("Już wypiłeś piwo");
        else
            Console.WriteLine("Wypijam piwo");
    })
    .PermitReentry(BeerTrigger.Drink);

beerMachine2.Configure(true)
    .Permit(BeerTrigger.Drink, false)
    .OnEntry(transition=>
    {
        if (transition.IsReentry)
            Console.WriteLine("Już uzupełniłeś piwo");
        else
            Console.WriteLine("Wypełniam piwem");
    })
    .PermitReentry(BeerTrigger.Fill);

Teraz przy każdej próbie ponownej akcji możemy wyświetlić odpowiedni komunikacji. Co więcej, gdybyś miał złożoną logikę maszyny np. coś można wcisnąć 3 razy, ale nie więcej to jak widzisz można by to było to tak tutaj  zrobić.

state.gif

Sama zmienna transition, które jest przekazywana w tych zdarzeniach zawiera w sobie informację o źródle i ilości wywołań. Mam także tutaj Source, czyli stan, z którego przechodzimy do Destination określający stan, do którego przechodzimy.

Zbiór stanów

Wracając do telefonu. Zapewne zauważyłeś, że podniesienie słuchawki zawiera się w innych stanach. Konfigurujesz to w następujący sposób.

call.Configure(PhoneState.OnHook)
    .SubstateOf(PhoneState.Connected)

Później korzystając z metody IsInState(state) możemy sprawdzać czy nasza maszyna jest danej grupie stanów.

Co jeszcze możemy zrobić:

Mamy możliwość blokowania przechodzenia w dany stan używając metody PermitIf().

Same wyzwalacze mogą być złożonymi obiektami, które później obsłużmy w zdarzeniach jak OnEntry().

Istnieje możliwość zapisana stanu swoje maszyny do innego źródła.

Podsumowanie wzorca State:

Jak widzisz wzorzec projektowy State pokazał na ciekawy świat maszyn stanów które robią coś więcej niż przechodzenie z jednego stanu do drugiego.

Czy są maszyny stanowe? Maszyna stanowa zawiera w sobie kolekcje dwóch parametrów : stan, wyzwalacz.  Nie jesteś ograniczony do typów wyliczeniowych. W niej kontrolujesz ilość wejść i wyjść. Same przejścia mogą być warunkowe oraz przekazywać zmienne pomocnicze.

W maszynie stany mogą mieć swoje grupy więc łatwo jesteś wstanie określić co dokładnie twoja maszyna robi.

Oczywiście korzystanie z takiej maszyny może wydawać się przeinżeniorowaniem  tego problemu, ale hej nadal to lepiej wygląda niż zbiór klas określający stan działania twojej aplikacji.