Tworzenie aplikacji biznesowych z grubsza polega na definiowaniu procesów, które mają za zadanie w określony sposób przetwarzać zapisane w systemie dane. Część z tych danych posiada swój stan (jeden z wielu), który zmienia sposób interpretacji zawartych wewnątrz informacji. Tematem dzisiejszego wpisu jest więc Maszyna Stanów – wzorzec, który upraszcza dużo w poruszonej wyżej kwestii.

Problem do rozwiązania

Każdy wzorzec projektowy służy do rozwiązania jakiegoś problemu z kodem. Zanim przejdziemy do tego, czym jest poruszany w dzisiejszym wpisie wzorzec, pochylmy się nad problemem życia codziennego, który wskazuje potrzebę jego użycia.

Wyobraźmy sobie, że mamy do czynienia z dużym systemem planowania dostaw zaopatrzenia sklepów oraz hurtowni. System ten składa się z:

  • Procesu przyjmowania i magazynowania towaru od dostawców hurtowych
  • Procesu przetwarzania (sortowania) i pakowania partii towaru
  • Procesu wydawania oraz transportu towaru do sklepów oraz hurtowni

Przyglądnijmy się każdemu procesowi po kolei.

Przyjmowanie towaru do magazynu

Przyjmując towar od producenta (na potrzeby przykładu przyjmijmy, że jest to towar rolniczy), dzielimy go na partie, które otrzymują stan gotowe do odbioru. Następnie, pracownik magazynu odpowiedzialny za odbiór klika w systemie, że odbiór jest procesowany, po czym wraz z zespołem odbiera towar, przekazując go do magazynu ze statusem w magazynie. Następnie pracownicy odpowiedzialni za magazynowanie pilnują, aby proces przetwarzania rozpoczynał się od tych partii, których termin zdatności do użycia jest najkrótszy. W tym celu, co jakiś czas w systemie uruchamiany jest proces, który weryfikuje każdą partię i ustawia jej jeden z kilku statusów: może czekać, do przeprocesowania lub pierwszeństwo procesowania.

Przetwarzanie partii towaru

Kolejnym etapem jest przetworzenie partii produktu. Dla uproszczenia, niech to będzie sortowanie po wadze/rozmiarze oraz pakowanie. Proces ten również będzie posiadał kilka statusów, które będą przełączanie przez różnych pracowników w systemie magazynowym. Status sortowanie wstępne zostanie ustawiony przez kierownika zmiany, która manualnie sortuje produkty nienadające się do sprzedaży od produktów, które mogą zostać przesortowane dalej. Po sortowaniu wstępnym wchodzimy w etap sortowania automatycznego po wadze/rozmiarze. Status sortowanie właściwe zostaje zatem ustawiony przez automat, podobnie jak status posortowano. W zależności od tego, czy proces pakowania jest prosty (2kg ziemniaków), czy bardziej skomplikowany (warzywna porcja rosołowa), statusy pakowanie oraz zapakowano będą przełączane przez automat, albo manualnie przez kierownika zmiany.

Transport towaru do punktów odbioru

W tym punkcie poszczególne partie towaru zostają rozdysponowywane pomiędzy punkty odbioru, czyli sklepy oraz hurtownie. Ze względu na to, że jedna partia może zostać rozesłana w różne miejsca, zostaje ona podzielona na palety. Każda z palet początkowo jest przechowywana ze statusem magazynowanie. Kiedy zostanie złożone zamówienie, paleta otrzymuje status zamówiono, a następnie wydano do transportu. Na końcu mamy statusy w transporcie oraz dostarczono. W samym systemie tworzy się również obiekt transportu, który podobnie co paleta ma swoje statusy, od których logiki zależą dalsze statusy palet. A dalej, od statusów wszystkich palet, zależą statusy poszczególnych partii.

Wyzwania dla zespołu programistów

Powyższy zespół procesów pokazuje kilka potencjalnych niebezpieczeństw i pułapek, które powinny zostać rozpatrzone przez leada zespołu:

  • Przedsiębiorstwo przetwórcze może zmieniać procesy w zależności od typu produktu. System powinien być prosty pod kątem dodawania/usuwania poszczególnych stanów. Powinien również pozwalać na swobodną modyfikację logiki, która kryje się pod każdym stanem.
  • Powinniśmy mieć pewność, że np. produkt nieposortowany nie zostanie spakowany, a już na pewno nie zostanie wydany do wysłania. Z programistycznego punktu widzenia jest to zwykły bug, jednakże przedsiębiorstwo przetwórcze fizycznie stanie, a towar, który jest wart duże pieniądze – zmarnuje się, oczekując na poprawkę w kodzie.
  • Powinniśmy mieć pewność, że tylko odpowiednie role mogą zmieniać status produktu (oraz co za tym idzie, uruchomić logikę za nim stojącą), kiedy ten znajduje się w odpowiednim stanie. Dodatkowo, nie powinno dochodzić do sytuacji, że np. automat zmieni przez przypadek status, który powinien zostać nadany przez osobę fizyczną.
  • W każdej chwili powinniśmy posiadać mechanizmy, które pomogą nam widzieć „z lotu ptaka” wszystkie procesy oraz zależności między nimi, tak, abyśmy mogli poddawać je poprawnej analizie.
  • Samo pojęcie „systemu” może oznaczać aplikację rozproszoną (mikroserwisy), czyli dodatkowo – potencjalnie wiele mniejszych systemów może wpływać na to, co dzieje się z partią towaru czy paletą.

Kiedy robimy coś z marszu, zmieniając w kodzie statusy w sposób rozproszony pomiędzy serwisami, po pewnym czasie przestajemy panować nad tym, co faktycznie dzieje się w przedsiębiorstwie. Nie jesteśmy w stanie odpowiedzieć sobie na ważne pytania, powodując potencjalnie ogromne straty naszego klienta.

Na szczęście istnieje wzorzec, który (poprawnie zastosowany) potrafi trzymać w ryzach to wszystko :). I o tym właśnie możecie przeczytać poniżej.

Wzorzec Maszyny Stanów

O ile większość wzorców projektowych przedstawia się w formie samo-deskryptywnego kodu, to w kontekście Maszyny Stanów najlepiej będzie przedstawić to za pomocą zbioru reguł.

Sama Maszyna Stanów powinna działać w określonym kontekście. Przykładem takiego kontekstu jest zamówienie, które ma różne stany obsługi, kurs online, która posiada różne etapy zaawansowania (mowa o etapie ukończenia kursu) oraz faktura, która może zostać utworzona, wysłana, opłacona, jak również poddana korekcie. Każda instancja tej maszyny powinna zajmować się tylko jednym stanem. Nie oznacza to, że ten stan składa się z tylko jednego pola, chociaż w większości przypadków biznesowych oraz implementacji tak właśnie jest.

Każda Maszyna Stanów powinna w obrębie kontekstu definiować unikalny zestaw stanów, w jakich może znajdować się obiekt kontekstu. Do tego celu najczęściej stosuje się stałe klas. Nie ma znaczenia, czy będą tam wartości liczbowe czy tekstowe. Z perspektywy wzorca, każdy ze stanów powinien mieć unikalną nazwę oraz wartość w obrębie kontekstu.

Kolejnym elementem, z jakiego składa się Maszyna Stanów są tranzycje pomiędzy poszczególnymi stanami. Każda tranzycja określa, z którego stanu możemy przejść na który. To dzięki tej funkcji możemy potencjalnie wygenerować graf, który przedstawi nam wszystkie możliwe scenariusze. A to z kolei daje nam bezpieczeństwo, że sytuacja przełączania się między stanami nie wymknie się spod kontroli.

Skoro o tranzycjach mowa, to każda z nich powinna posiadać miejsce na podczepianie logiki, która powinna zostać wykonana w momentach przed oraz po wykonaniu tranzycji. Najczęściej do tego celu stosuje się mechanizm callbacków oraz event listenerów / subscriberów. Jeżeli w systemie jest wiele miejsc zainteresowanych konkretną tranzycją, to ich logika powinna być niezależna od logiki pozostałych zainteresowanych tą tranzycją mechanizmów.

Dostęp do maszyny „z zewnątrz”

Ostatnim elementem, na który pragnę zwrócić uwagę jest sposób, w który korzystamy z Maszyny Stanów. Nie powinniśmy móc utworzyć egzemplarza maszyny bez obiektu kontekstu, którego maszyna dotyczy. Nie powinniśmy mieć również możliwości podmiany kontekstu na inny. Jeżeli potrzebujemy uruchomić maszynę na innym obiekcie kontekstu, to powinniśmy utworzyć nowy egzemplarz maszyny. Samo korzystanie z maszyny „z zewnątrz” powinno sprowadzać się wyłącznie do zmiany stanu. Jeżeli w definicji maszyny nie istnieje tranzycja, która pozwoliłaby na przejście pomiędzy stanami, to powinien zostać wyrzucony odpowiedni wyjątek.

Dodatkowo, nie ma przeciwwskazań, aby jedna definicja Maszyny Stanów zależała od definicji drugiej Maszyny. Odbywać się to powinno poprzez tworzenie egzemplarza (oraz pracy na nim) jednej maszyny, wewnątrz logiki tranzycyjnej drugiej maszyny.

Zalety stosowania Maszyny Stanów

Największą zaletą Maszyny Stanów jest w zasadzie to, że wszystko, co dotyczy zmiany stanu, mamy w jednym miejscu. Ale jest kilka dodatkowych zalet, które pośrednio z tego wynikają:

  • Dla skomplikowanych procesów, w łatwy sposób potrafimy znaleźć miejsca problematyczne
  • Dodawanie nowych, usuwanie nieaktualnych stanów, oraz zależności między nimi nie stanowi dla nas większego problemu
  • Lepsza kontrola nad logiką reagującą na zmiany stanów; w dalszej konsekwencji istnieje również możliwość na przeniesienie fragmentów logiki do procesów asynchronicznych
  • Lepsze zarządzanie architekturą aplikacji. Wydzielenie osobnego komponentu (ba, nawet serwisu!) stanowiącego zmiany stanów (oraz logikę aplikacji) jest bardzo proste
  • Scenariusze Behata będą o wiele prostsze, co pozwoli na lepszą analizę procesów osobom nieprogramującym

Biblioteki gotowe do użycia

Sam opis wzorca może nie odzwierciedlać w naszej wyobraźni tego, w jaki sposób on wygląda w trakcie „pracy”. Przygotowałem dwie bardzo popularne implementacje, do których przejrzenia serdecznie zapraszam 🙂

Projekt Winzou State Machine

Bardzo przyjazną implementacją Maszyny Stanów jest Winzou State Machine. Jest to dosyć prosta biblioteka, której konfiguracja odbywa się poprzez przesłanie odpowiedniej tablicy do konstruktora egzemplarza maszyny. Wewnątrz tej konfiguracji definiujemy stany, tranzycje oraz callbacki.

W razie, gdyby komuś pracującemu na Symfony nie spodobało się, że konfiguracja sprowadza się do przesłania tablicy – mam dobrą nowinę. Mamy Symfonowy bundle, który arraykę zamienia na plik konfiguracyjny 🙂 Samo tworzenie egzemplarza odbywa się tutaj poprzez uzycie odpowiedniej fabryki. Idąc za README projektu, wygląda to następująco:

// example.php

public function myAwesomeAction($id, \SM\Factory\Factory $factory)
{
    // Get your domain object
    $article = $this->getRepository('MyAwesomeBundle:Article')->find($id);
    
    // Get the state machine for this object, and graph called "simple"
    $articleSM = $factory->get($article, 'simple');

    // Check if a transition can be applied: returns true or false
    $articleSM->can('a_transition_name');

    // Apply a transition
    $articleSM->apply('a_transition_name');

    // Get the actual state of the object
    $articleSM->getState();

    // Get all available transitions
    $articleSM->getPossibleTransitions();
}

Sylius jako przykład poprawnej implementacji

Zdecydowałem się na prezentację projektu Winzou w pierwszeństwie, ponieważ mam z nim na co dzień do czynienia podczas pracy z Syliusem. Sylius, jako topowe i przemyślane narzędzie klasy e-commerce, posiada kilka miejsc, w których wzorzec Maszyny Stanów sprawdza się bardzo dobrze. Są to:

  • Zamówienia
  • Płatności
  • Dostawy
  • Promocje katalogu produktów
  • Oceny produktów

Ich pliki konfiguracyjne znajdziecie tutaj, tutaj, tutaj i tutaj.

Komponent Symfony Workflow

Drugim, bardziej znanym narzędziem implementującym wzorzec Maszyny Stanów jest komponent Symfony Workflow. Jako komponent, Workflow jest bardziej zintegrowany z Symfony. Z marszu posiada możliwość konfiguracji w YAML oraz XML.

Oprócz kilku drobnych różnic w nazewnictwach, pomiędzy projektem Winzou oraz Workflow istnieje kilka gruntownych różnic. Jedną z nich jest sposób podpięcia logiki pod tranzycje. O ile projekt Winzou pozwala na definicję callbacków, to Symfony Workflow pozwala nam na pracę skoncentrowaną na systemie Eventów. Komponent ten jest bezpośrednio zintegrowany z komponentem Event Dispatchera, dzięki któremu w momencie uruchamiania tranzycji są wysyłane na szynę zdarzenia. A na te z kolei możemy nasłuchiwać Event Listenerami oraz Subscriberami.

Dodatkowym atutem komponentu Workflow jest możliwość wydrukowania grafów, co daje nam dużą możliwość analityczną oraz bezpieczeństwo logiki biznesowej. Grafy drukujemy do postaci obrazków. Mamy do dyspozycji trzy drivery, od których zależy to, w jaki sposób ten graf bedzie wyglądał.

Mowa o:

Każde z tych narzędzi należy do świata Open Source, zatem zachęcam każdego z Was do poszperania, może znajdziecie tam jakieś ciekawe opcje customizacji drukowanych grafów 🙂

Profesjonalista, zajmujący się na co dzień aplikacjami biznesowymi w ekosystemie PHP. Jego pasją jest odkrywanie nowych konceptów programistycznych oraz wzorce architektoniczne. Uwielbia również pisać testy, gdyż jak sam uważa, dobry kod to przetestowany kod.

Comments are closed.