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
Zobaczmy jak szybko możemy napisać podobny przykład z telefonem korzystając z tej biblioteki.
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:
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ć.
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.