Wiosenna WYPRZEDAŻ

Stan

Znany też jako: State

Cel

Stan to behawioralny wzorzec projektowy pozwalający obiektowi zmienić swoje zachowanie gdy zmieni się jego stan wewnętrzny. Wygląda to tak, jakby obiekt zmienił swoją klasę.

Wzorzec projektowy Stan

Problem

Wzorzec Stan jest powiązany z koncepcją Automatu skończonego .

Automat skończony

Automat skończony.

Sednem tej koncepcji jest to, że w dowolnym momencie istnieje skończona liczba stanów w których program może się znajdować. W każdym z nich program zachowuje się różnie i może zostać przełączony z jednego stanu w drugi natychmiastowo. Możliwe stany, w jakich obiekt może się znaleźć, zależą od bieżącego stanu. Liczba reguł przełączeń, zwanych przejściami również jest skończona i są one z góry określone.

Można też zastosować to podejście wobec obiektów. Wyobraźmy sobie klasę Dokument. Dokument może znajdować się w jednym z trzech stanów: Szkic, Korekta i Opublikowany. Metoda publikuj dokumentu działa nieco inaczej zależnie od jego stanu:

  • W przypadku Szkicu przenosi dokument do moderacji.
  • W stanie Korekta czyni dokument dostępnym publicznie, ale tylko jeśli bieżący użytkownik jest administratorem.
  • W stanie Opublikowany nie robi nic.
Możliwe stany obiektu dokument

Możliwe stany i przejścia między stanami obiektu dokument.

Maszyny stanów są zwykle implementowane za pomocą wielu struktur warunkowych (if lub switch) które wybierają odpowiednie zachowanie zależnie od bieżącego stanu dokumentu. Zazwyczaj “stan” jest tylko zestawem wartości pól obiektu. Nawet jeśli nie wiesz nic o automatach skończonych, prawdopodobnie przynajmniej raz implementowałeś stan. Czy poniższy kawałek kodu coś ci przypomina?

class Document is
    field state: string
    // ...
    method publish() is
        switch (state)
            "draft":
                state = "moderation"
                break
            "moderation":
                if (currentUser.role == "admin")
                    state = "published"
                break
            "published":
                // Nie rób nic.
                break
    // ...

Największa słabość maszyny stanów opartej na instrukcjach warunkowych ujawnia się w miarę dodawania kolejnych stanów oraz zachowań zależnych od tych stanów do klasy Dokument. Większość metod będzie zawierała olbrzymie instrukcje warunkowe wybierające stosowne zachowanie się metody zależnie od bieżącego stanu. Taki kod jest trudny w utrzymaniu, ponieważ każda zmiana logiki przechodzenia między stanami może wymagać zmian instrukcji warunkowych w każdej z metod.

Problem narasta wraz z rozbudową projektu. Trudno jest przewidzieć wszystkie możliwe stany i przejścia między nimi na etapie projektowania. Dlatego też prosta maszyna stanów, zbudowana z ograniczonej liczby instrukcji warunkowych, może z czasem spuchnąć do niebotycznych rozmiarów.

Rozwiązanie

Wzorzec Stan proponuje stworzenie nowych klas dla każdego z możliwych stanów obiektu oraz ekstrakcję wszystkich zachowań zależnych od stanu do tychże klas.

Zamiast implementować wszystkie zachowania samodzielnie, pierwotny obiekt, zwany kontekstem, przechowuje odniesienie do jednego z obiektów-stanów który w danej chwili reprezentuje jego bieżący stan i deleguje mu zadania związane z tym stanem.

Dokument deleguje pracę obiektowi stanu

Dokument deleguje pracę obiektowi stanu.

Aby przełączyć kontekst do innego stanu, zamieniamy aktywny obiekt stanu na inny obiekt reprezentujący nowy stan. Jest to możliwe wyłącznie gdy wszystkie klasy stanu są zgodne pod względem interfejsu, zaś kontekst współpracuje z tymi obiektami tylko poprzez ów interfejs.

Taka struktura może przypominać wzorzec Strategia, ale z jedną kluczową różnicą. W przypadku wzorca Stan, poszczególne stany mogą być świadome siebie nawzajem i inicjować przejścia z jednego stanu w drugi, zaś strategie prawie nigdy nie wiedzą nic o sobie.

Analogia do prawdziwego życia

Przyciski i przełączniki w twoim smartfonie zachowują się w różny sposób, w zależności od bieżącego stanu urządzenia:

  • Gdy telefon jest odblokowany, wciskanie przycisków wywołuje różne funkcje.
  • Gdy telefon jest zablokowany, wciskanie przycisków wyświetli ekran służący odblokowaniu.
  • Gdy bateria telefonu jest na wyczerpaniu, wciśnięcie dowolnego przycisku wyświetli ekran ładowania.

Struktura

Struktura wzorca StanStruktura wzorca Stan
  1. Kontekst przechowuje odniesienie do jednego z konkretnych obiektów-stanów i deleguje mu zadania specyficzne dla danego stanu. Kontekst porozumiewa się z obiektem stanu za pośrednictwem interfejsu stanu. Kontekst eksponuje metodę setter, przez którą przekazuje się obiekt nowego stanu dla kontekstu.

  2. Interfejs Stanu deklaruje metody specyficzne dla stanu. Metody te powinny być sensowne dla konkretnych stanów, ponieważ nie chcemy, aby któreś ze stanów posiadały bezużyteczne metody które nie zostaną nigdy wywołane.

  3. Konkretne Stany dostarczają swoje implementacje metod specyficznych dla poszczególnych stanów. Aby uniknąć powtórzeń podobnego kodu w wielu stanach, można utworzyć pośrednie klasy abstrakcyjne które hermetyzują jakieś wspólne zachowania.

    Obiekty stanu mogą przechowywać referencje wsteczne do obiektu kontekst. Za pośrednictwem tego odniesienia stan może pobrać dowolne informacje z obiektu kontekst, a także zainicjować zmianę stanu.

  4. Zarówno kontekst, jak i konkretne stany mogą ustawić kolejny stan kontekstu i wykonać samą zmianę stanu poprzez zamianę obiektu stanu powiązanego z kontekstem na inny.

Pseudokod

W tym przykładzie, wzorzec Stan pozwala tym samym kontrolkom odtwarzacza multimedialnego zachowywać się nieco inaczej, zależnie od stanu odtwarzania.

Struktura przykładu użycia wzorca Stan

Przykład zmiany zachowania się obiektu zależnie od obiektu stanu.

Główny obiekt odtwarzacza jest zawsze powiązany z obiektem stanu, który wykonuje większość zadań odtwarzacza. Niektóre czynności zamieniają bieżący obiekt stanu na inny, co przy okazji zmienia reakcje odtwarzacza na polecenia od użytkownika.

// Klasa AudioPlayer pełni rolę kontekstu. Posiada także
// odniesienie do instancji jednej z klas-stanów reprezentującej
// bieżący stan odtwarzacza audio.
class AudioPlayer is
    field state: State
    field UI, volume, playlist, currentSong

    constructor AudioPlayer() is
        this.state = new ReadyState(this)

        // Kontekst deleguje obsługę danych wejściowych
        // użytkownika obiektowi stanu. Oczywiście wynik zależy
        // od tego jaki stan jest aktualnie aktywny, ponieważ
        // każdy ze stanów może obsługiwać dane wejściowe nieco
        // inaczej.
        UI = new UserInterface()
        UI.lockButton.onClick(this.clickLock)
        UI.playButton.onClick(this.clickPlay)
        UI.nextButton.onClick(this.clickNext)
        UI.prevButton.onClick(this.clickPrevious)

    // Inne obiekty muszą być w stanie przełączyć aktywny stan
    // odtwarzacza audio.
    method changeState(state: State) is
        this.state = state

    // Metody interfejsu użytkownika delegują wykonanie
    // aktywnemu stanowi.
    method clickLock() is
        state.clickLock()
    method clickPlay() is
        state.clickPlay()
    method clickNext() is
        state.clickNext()
    method clickPrevious() is
        state.clickPrevious()

    // Stan może wywoływać jakieś metody-usługi kontekstu.
    method startPlayback() is
        // ...
    method stopPlayback() is
        // ...
    method nextSong() is
        // ...
    method previousSong() is
        // ...
    method fastForward(time) is
        // ...
    method rewind(time) is
        // ...


// Klasa bazowa stanu deklaruje metody które muszą być
// zaimplementowane przez wszystkie konkretne stany. Posiada też
// referencję zwrotną do obiektu-kontekstu skojarzonego ze
// stanem. Stany mogą za pomocą tej referencji zwrotnej wywołać
// przejście kontekstu z jednego stanu w inny.
abstract class State is
    protected field player: AudioPlayer

    // Kontekst przekazuje siebie do konstruktora stanu. Pomoże
    // to stanowi pozyskiwać różne użyteczne dane kontekstu
    // jeśli zaistnieje potrzeba.
    constructor State(player) is
        this.player = player

    abstract method clickLock()
    abstract method clickPlay()
    abstract method clickNext()
    abstract method clickPrevious()


// Konkretne stany implementują różnorakie zachowania związane z
// danym stanem kontekstu.
class LockedState extends State is

    // Gdy odblokuje się zablokowany odtwarzacz, może on znaleźć
    // się w którymś z dwóch stanów.
    method clickLock() is
        if (player.playing)
            player.changeState(new PlayingState(player))
        else
            player.changeState(new ReadyState(player))

    method clickPlay() is
        // Zablokowany, więc nie rób nic.

    method clickNext() is
        // Zablokowany, więc nie rób nic.

    method clickPrevious() is
        // Zablokowany, więc nie rób nic.


// Konkretne stany mogą też wyzwolić przejście kontekstu z
// jednego stanu w inny.
class ReadyState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.startPlayback()
        player.changeState(new PlayingState(player))

    method clickNext() is
        player.nextSong()

    method clickPrevious() is
        player.previousSong()


class PlayingState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.stopPlayback()
        player.changeState(new ReadyState(player))

    method clickNext() is
        if (event.doubleclick)
            player.nextSong()
        else
            player.fastForward(5)

    method clickPrevious() is
        if (event.doubleclick)
            player.previous()
        else
            player.rewind(5)

Zastosowanie

Stosuj wzorzec Stan gdy masz do czynienia z obiektem którego zachowanie jest zależne od jego stanu, liczba możliwych stanów jest wielka, a kod specyficzny dla danego stanu często ulega zmianom.

Wzorzec proponuje ekstrakcję całego kodu właściwego poszczególnym stanom do zestawu osobnych klas. W wyniku tego można będzie dodawać nowe stany lub zmieniać istniejące niezależnie od siebie, zmniejszając koszty utrzymania.

Stosuj ten wzorzec gdy masz klasę zaśmieconą rozbudowanymi instrukcjami warunkowymi zmieniającymi zachowanie klasy zależnie od wartości jej pól.

Wzorzec Stan pozwala wyekstrahować rozgałęzienia tych instrukcji warunkowych do metod które znajdą się w klasach reprezentujących poszczególne stany. W ten sposób uprzątnąć można przy okazji tymczasowe pola i metody pomocnicze związane z kodem odnoszącym się do stanów.

Wzorzec Stan pomaga poradzić sobie z dużą ilością kodu który się powtarza w wielu stanach i przejściach między stanami automatu skończonego, bazującego na instrukcjach warunkowych.

Wzorzec Stan pozwala komponować hierarchie klas stanów i zmniejszyć ilość powtórzeń kodu poprzez ekstrakcję wspólnego kodu do abstrakcyjnych klas bazowych.

Jak zaimplementować

  1. Zdecyduj która klasa będzie pełniła rolę kontekstu. Może to być istniejąca klasa zawierająca już jakiś kod zależny od stanu obiektu, ale może to być także nowa klasa, jeśli kod specyficzny dla stanów jest rozrzucony po wielu klasach.

  2. Zadeklaruj interfejs stanu. Mimo że może on odzwierciedlać wszystkie metody zadeklarowane w kontekście, skup się tylko na tych, które dotyczą zachowania specyficznego dla danego stanu.

  3. Dla każdego faktycznego stanu stwórz klasę wywodzącą się z interfejsu stanu. Następnie przejrzyj metody kontekstu i wyekstrahuj cały kod dotyczący tego stanu do nowo utworzonej klasy.

    Przenosząc kod do klasy stanu możesz zauważyć, że zależy on od prywatnych składowych klasy kontekstu. Można sobie z tym poradzić w następujący sposób:

    • Uczyń te pola lub metody publicznymi.
    • Zmień ekstrahowane zachowanie na publiczną metodę kontekstu i wywołuj ją z klasy stanu. To brzydkie, ale szybkie rozwiązanie, które można później naprawić.
    • Zagnieźdź klasy stanów w klasie kontekstu, ale tylko jeśli używany język programowania obsługuje zagnieżdżanie klas.
  4. W klasie kontekstu dodaj pole przechowujące odniesienie do interfejsu stanu i publicznie dostępną metodę setter, która umożliwia nadpisanie wartości tego pola.

  5. Przejrzyj metodę kontekstu raz jeszcze i zamień puste instrukcje warunkowe dotyczące stanu na wywołania stosownych metod obiektu stanu.

  6. Aby przełączyć stan kontekstu, utwórz instancję jednej z klas stanu i przekaż ją kontekstowi. Można tego dokonać w ramach samego kontekstu, w którymś ze stanów, bądź po stronie klienta. Za każdym razem, gdy to się dzieje, klasa staje się zależna od konkretnej klasy stanu której instancja powstaje.

Zalety i wady

  • Zasada pojedynczej odpowiedzialności. Zorganizuj kod związany z konkretnymi stanami w osobne klasy.
  • Zasada otwarte/zamknięte. Można wprowadzać nowe stany bez zmiany istniejących klas stanu lub kontekstu.
  • Upraszcza kod kontekstu eliminując obszerne instrukcje warunkowe automatu skończonego.
  • Zastosowanie tego wzorca może być przesadą jeśli mamy do czynienia zaledwie z kilkoma stanami i rzadkimi zmianami.

Powiązania z innymi wzorcami

  • Most, Stan, Strategia (i w pewnym stopniu Adapter) mają podobną strukturę. Wszystkie oparte są na kompozycji, co oznacza delegowanie zadań innym obiektom. Jednak każdy z tych wzorców rozwiązuje inne problemy. Wzorzec nie jest bowiem tylko receptą na ustrukturyzowanie kodu w pewien sposób, lecz także informacją dla innych deweloperów o charakterze rozwiązywanego problemu.

  • Stan można uważać za rozszerzenie Strategii. Oba wzorce oparte są o kompozycję: zmieniają zachowanie kontekstu przez delegowanie części zadań obiektom pomocniczym. Strategia czyni te obiekty całkowicie niezależnymi i nieświadomymi siebie nawzajem. Jednakże Stan nie ogranicza zależności pomiędzy konkretnymi stanami i pozwala im zmieniać stan kontekstu według uznania.

Przykłady kodu

Stan w języku C# Stan w języku C++ Stan w języku Go Stan w języku Java Stan w języku PHP Stan w języku Python Stan w języku Ruby Stan w języku Rust Stan w języku Swift Stan w języku TypeScript