Komputery Okna Internet

Państwo. Klasy stanów dla każdego stanu

Behawioralny wzorzec projektowy. Stosuje się go w przypadkach, gdy podczas wykonywania programu obiekt musi zmienić swoje zachowanie w zależności od swojego stanu. Klasyczna implementacja polega na utworzeniu podstawowej klasy abstrakcyjnej lub interfejsu zawierającego wszystkie metody i jedną klasę dla każdego możliwego stanu. Wzorzec jest szczególnym przypadkiem zalecenia „zamień instrukcje warunkowe na polimorfizm”.

Wydawałoby się, że wszystko jest zgodne z książką, ale jest niuans. Jak poprawnie zaimplementować metody, które nie są istotne dla danego stanu? Na przykład, jak usunąć przedmiot z pustego koszyka lub zapłacić za pusty koszyk? Zazwyczaj każda klasa stanu implementuje tylko odpowiednie metody i zgłasza InvalidOperationException w innych przypadkach.

Naruszenie zasady zastępowania Liskova osobą. Yaron Minsky zaproponował alternatywne podejście: uczynić nielegalne państwa niereprezentowalnymi. Umożliwia to przeniesienie sprawdzania błędów ze środowiska wykonawczego do czasu kompilacji. Jednakże przepływ sterowania w tym przypadku będzie zorganizowany w oparciu o dopasowywanie wzorców, a nie przy użyciu polimorfizmu. Na szczęście, .

Więcej szczegółów na przykładzie tematu F# uczynić nielegalne państwa niereprezentowalnymi ujawniono na stronie internetowej Scotta Vlashina.

Rozważmy implementację „stanu” na przykładzie koszyka. C# nie ma wbudowanego typu unii. Oddzielmy dane i zachowania. Zakodujemy sam stan za pomocą wyliczenia, a zachowanie jako osobną klasę. Dla wygody zadeklarujmy atrybut łączący wyliczenie z odpowiednią klasą zachowania, bazową klasą „stanu”, i dodajmy metodę rozszerzenia, aby przejść z wyliczenia do klasy zachowania.

Infrastruktura

klasa publiczna StateAttribute: Atrybut ( typ publiczny StateType ( get; ) public StateAttribute(Type stateType) ( StateType = stateType ?? wrzuć nowy ArgumentNullException(nameof(stateType)); ) ) publiczna klasa abstrakcyjna Stan gdzie T: klasa ( chroniony stan (jednostka T) ( Entity = jednostka ? rzucaj nowy ArgumentNullException (nazwa (jednostka)); ) chroniona jednostka T ( get; ) ) publiczna klasa statyczna StateCodeExtensions ( publiczny stan statyczny Określić (ten kod stanu Enum, jednostka obiektu) gdzie T: klasa // tak, tak odbicie jest powolne. Zastąp skompilowanym drzewem wyrażeń // lub IL Emit i będzie szybko => (State ) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute ().StateType, jednostka); )

Tematyka

Zadeklarujmy encję „koszyk”:

Interfejs publiczny IHasState gdzie TEntity: klasa ( TStateCode StateCode ( get; ) Stan Stan ( get; ) ) publiczna klasa częściowa Koszyk: IHasState ( public User User ( get; chroniony zestaw; ) public CartStateCode StateCode ( get; chroniony zestaw; ) public State State => StateCode.ToState (Ten); public decimal Suma ( get; chroniony zestaw; ) chroniona wirtualna kolekcja ICollection Produkty (get; set; ) = nowa lista (); // Tylko ORM chroniony koszyk() ( ) publiczny koszyk (użytkownik) ( użytkownik = użytkownik ?? wrzuć nowy wyjątek ArgumentNullException(nazwa(użytkownik)); StateCode = StateCode = CartStateCode.Empty; ) publiczny koszyk (użytkownik użytkownika, IEnumerable Produkty): this(user) ( StateCode = StateCode = CartStateCode.Empty; foreach (var produkt w produktach) ( Products.Add(product); ) ) publiczny koszyk (użytkownik użytkownika, IEnumerable Produkty, suma dziesiętna): this(użytkownik, produkty) ( if (suma<= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
Zaimplementujemy jedną klasę dla każdego stanu koszyka: pusty, aktywny i płatny, ale nie będziemy deklarować wspólnego interfejsu. Niech każde państwo wdraża tylko odpowiednie zachowanie. Nie oznacza to, że klasy PusteCartState, ActiveCartState i PaidCartState nie mogą implementować tego samego interfejsu. Mogą, ale taki interfejs musi zawierać tylko metody, które są dostępne w każdym stanie. W naszym przypadku metoda Add jest dostępna w stanach DesertCartState i ActiveCartState, dzięki czemu możemy je odziedziczyć z abstrakcyjnej bazy AddableCartStateBase. Możesz jednak dodawać produkty tylko do niezapłaconego koszyka, więc nie będzie wspólnego interfejsu dla wszystkich stanów. W ten sposób gwarantujemy, że w czasie kompilacji w naszym kodzie nie będzie żadnego wyjątku InvalidOperationException.

Publiczna klasa częściowa Cart ( public enum CartStateCode: bajt ( pusty, aktywny, płatny ) interfejs publiczny IAddableCartState ( ActiveCartState Add (produkt produktu); IEnumerable Produkty ( get; ) ) interfejs publiczny INotEmptyCartState ( IEnumerable Produkty ( get; ) dziesiętny Total ( get; ) ) publiczna klasa abstrakcyjna AddableCartState: State , IAddableCartState ( chroniony AddableCartState(encja koszyka): base(encja) ( ) public ActiveCartState Add(Product produkt) ( Entity.Products.Add(product); Entity.StateCode = CartStateCode.Active; return (ActiveCartState)Entity.State; ) publiczny IEnumerable Produkty => Jednostka.Produkty; ) klasa publiczna DesertCartState: AddableCartState ( public DesertCartState(encja koszyka): base(encja) ( ) ) klasa publiczna ActiveCartState: AddableCartState, INotEmptyCartState ( public ActiveCartState(encja koszyka): base(encja) ( ) public PaidCartState Pay(suma dziesiętna) ( Entity.Total = suma; Entity.StateCode = CartStateCode.Paid; zwrot (PaidCartState)Entity.State; ) stan publiczny Remove(Product produkt) ( Entity.Products.Remove(product); if(!Entity.Products.Any()) ( Entity.StateCode = CartStateCode.Empty; ) return Entity.State; ) public DesertCartState Clear() ( Entity. Products.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State; ) publiczna liczba dziesiętna Total => Products.Sum(x => x.Price); ) klasa publiczna PaidCartState: Stan , INotEmptyCartState (publiczny IEnumerable Produkty => Jednostka.Produkty; public decimal Total => Entity.Total; public PaidCartState(jednostka koszyka): baza(jednostka) ( ) ) )
Stany są deklarowane zagnieżdżone ( zagnieżdżone) zajęcia nie są przypadkowe. Klasy zagnieżdżone mają dostęp do chronionych elementów klasy Cart, co oznacza, że ​​nie musimy rezygnować z hermetyzacji encji, aby zaimplementować zachowanie. Aby uniknąć bałaganu w pliku klasy encji, podzieliłem deklarację na dwie części: Cart.cs i CartStates.cs, używając słowa kluczowego części.

Public ActionResult GetViewResult(Stan cartState) ( przełącznik (cartState) ( case Cart.ActiveCartState activeState: return View("Active", activeState); case Cart.EmptyCartState pustyState: return View("Empty", pustyState); case Cart.PaidCartStatepaidCartState: return View(" Paid”,paidCartState); domyślnie: wyślij nowy wyjątek InvalidOperationException(); ) )
W zależności od stanu koszyka będziemy korzystać z różnych widoków. W przypadku pustego koszyka wyświetlimy komunikat „Twój koszyk jest pusty”. Aktywny koszyk będzie zawierał listę produktów, możliwość zmiany ilości produktów i usunięcia części z nich, przycisk „złóż zamówienie” oraz całkowitą kwotę zakupu.

Koszyk płatny będzie wyglądał tak samo jak koszyk aktywny, ale bez możliwości edycji czegokolwiek. Fakt ten można zauważyć podświetlając interfejs INotEmptyCartState. W ten sposób nie tylko pozbyliśmy się naruszenia zasady podstawienia Liskowa, ale także zastosowaliśmy zasadę separacji interfejsów.

Wniosek

W kodzie aplikacji możemy popracować z łączami interfejsu IAddableCartState i INotEmptyCartState, aby ponownie wykorzystać kod odpowiedzialny za dodawanie artykułów do koszyka i wyświetlanie pozycji w koszyku. Uważam, że dopasowywanie wzorców nadaje się tylko do przepływu sterowania w C#, gdy nie ma nic wspólnego między typami. W innych przypadkach praca z łączem podstawowym jest wygodniejsza. Podobną technikę można zastosować nie tylko do kodowania zachowania jednostki, ale także do .

Czas na wyznanie: z tym głównym trochę przesadziłem. Miało chodzić o wzorzec projektowy GoF State. Nie mogę jednak mówić o jego zastosowaniu w grach, nie dotykając samej koncepcji maszyny o skończonych stanach(lub „FSM”). Ale kiedy już się w to zagłębiłem, zdałem sobie sprawę, że muszę pamiętać hierarchiczna maszyna stanu Lub automat hierarchiczny I automat z pamięcią magazynka (automaty pushdown).

Jest to bardzo szeroki temat, więc aby ten rozdział był jak najkrótszy, pominę kilka oczywistych przykładów kodu, a niektóre luki będziesz musiał sam wypełnić. Mam nadzieję, że to nie sprawi, że będą one mniej zrozumiałe.

Nie musisz się denerwować, jeśli nigdy nie słyszałeś o maszynach o skończonych stanach. Są dobrze znane twórcom sztucznej inteligencji i hakerom komputerowym, ale mało znane w innych dziedzinach. Moim zdaniem zasługują na większe uznanie, dlatego chcę pokazać Wam kilka problemów, jakie rozwiązują.

Wszystko to są echa dawnych początków sztucznej inteligencji. W latach 50. i 60. sztuczna inteligencja skupiała się głównie na przetwarzaniu struktur językowych. Wiele technologii stosowanych we współczesnych kompilatorach zostało wynalezionych do analizowania języków ludzkich.

Wszyscy tam byliśmy

Załóżmy, że pracujemy nad małą platformówką typu side-scrolling. Naszym zadaniem jest wymodelowanie bohaterki, która będzie awatarem gracza w świecie gry. Oznacza to, że musi reagować na działania użytkownika. Naciśnij B, a ona podskoczy. Całkiem proste:

void Heroine::handleInput(Wejście wejściowe) ( if (input == PRESS_B) ( yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) )

Zauważyłeś błąd?

Nie ma tu żadnego kodu zapobiegającego „skakaniu w powietrze”; naciskaj B, gdy jest w powietrzu, a będzie latać w górę raz za razem. Najprostszym sposobem rozwiązania tego problemu jest dodanie flagi logicznej isJumping_ do Heroine, która będzie śledzić, kiedy bohaterka skoczyła:

void Heroine::handleInput(Wejście wejściowe) ( if (input == PRESS_B) ( if (!isJumping_) ( isJumping_ = true ; // Skok... ) ) )

Potrzebujemy także kodu, który ustawi isJumping_ z powrotem na false, gdy bohaterka ponownie dotknie ziemi. Dla uproszczenia pomijam ten kod.

void Heroine::handleInput(Wejście wejściowe) ( if (wejście == PRESS_B) ( // Przejdźmy, jeśli jeszcze tego nie zrobiliśmy...) else if (input == PRESS_DOWN) ( if (!isJumping_) ( setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( setGraphics(IMAGE_STAND); ) )

Czy zauważyłeś tutaj błąd?

Za pomocą tego kodu gracz może:

  1. Naciśnij, aby wykonać przysiad.
  2. Naciśnij B, aby wyskoczyć z pozycji siedzącej.
  3. Opuść się będąc w powietrzu.

W tym samym czasie bohaterka przełączy się na grafikę stojącą w powietrzu. Będziemy musieli dodać kolejną flagę...

void Heroine::handleInput(Wejście wejściowe) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( isDucking_ = false ; setGraphics(IMAGE_STAND); ) ) )

Teraz byłoby wspaniale dodać dla bohaterki możliwość ataku wślizgiem, gdy gracz naciśnie przycisk w powietrzu:

void Heroine::handleInput(Wejście wejściowe) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) else ( isJumping_ = false ; setGraphics(IMAGE_DIVE); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( // Stojąc... ) ) )

Znowu szukam błędów. Znalazłeś?

Mamy czek, który uniemożliwia skakanie w powietrzu, ale nie podczas wślizgu. Dodawanie kolejnej flagi...

Coś jest nie tak z tym podejściem. Za każdym razem, gdy dotykamy kodu, coś się psuje. Będziemy musieli dodać o wiele więcej ruchu, a nawet nie mamy pieszy nie, ale przy takim podejściu będziemy musieli pokonać mnóstwo błędów.

Programiści, których wszyscy idealizujemy i którzy tworzą świetny kod, wcale nie są supermenami. Po prostu rozwinęli w sobie instynkt kodowania, który grozi wprowadzeniem błędów, i starają się ich unikać, gdy tylko jest to możliwe.

Złożone rozgałęzienia i zmiany stanów to dokładnie typy kodu, których powinieneś unikać.

Maszyny o skończonych stanach są naszym wybawieniem

W przypływie frustracji zdejmujesz wszystko z biurka oprócz ołówka i papieru i zaczynasz rysować schemat blokowy. Rysujemy prostokąt dla każdej akcji, którą może wykonać bohaterka: stania, skakania, kucania i toczenia. Aby mógł reagować na naciśnięcia klawiszy w dowolnym stanie, rysujemy strzałki pomiędzy tymi prostokątami, etykietujemy przyciski nad nimi i łączymy stany ze sobą.

Gratulacje, właśnie utworzyłeś maszyna stanu (maszyna skończona). Pochodzą z dziedziny informatyki tzw teoria automatów (teoria automatów), do którego rodziny konstrukcji zalicza się także słynna maszyna Turinga. FSM jest najprostszym członkiem tej rodziny.

Konkluzja jest następująca:

    Mamy stały zestaw stwierdza, który może zawierać karabin maszynowy. W naszym przykładzie są to: stanie, skakanie, kucanie i tarzanie się.

    Maszyna może być tylko w środku jeden stan w dowolnym momencie. Nasza bohaterka nie potrafi jednocześnie skakać i stać. Właściwie FSM jest używany przede wszystkim, aby temu zapobiec.

    Podciąg wejście Lub wydarzenia, przesyłane do maszyny. W naszym przykładzie jest to wciskanie i zwalnianie przycisków.

    Każde państwo ma zestaw przejściowy, z których każdy jest powiązany z wejściem i wskazuje stan. Gdy nastąpi wprowadzenie danych przez użytkownika, jeśli odpowiadają one bieżącemu stanowi, maszyna zmienia swój stan na wskazany przez strzałkę.

    Na przykład, jeśli naciśniesz w pozycji stojącej, nastąpi przejście do stanu przysiadu. Naciśnięcie podczas skoku zmienia stan do ataku. Jeśli w bieżącym stanie nie jest zapewnione żadne przejście dla wejścia, nic się nie dzieje.

To w najczystszej postaci cały banan: stany, wejścia i przejścia. Można je przedstawić w formie schematu blokowego. Niestety kompilator nie zrozumie takich bazgrołów. Więc jak wtedy wprowadzić w życie maszyna skończona? Gang Czterech oferuje własną wersję, ale zaczniemy od jeszcze prostszej.

Moją ulubioną analogią FSM jest stary tekstowy quest Zork. Masz świat składający się z pomieszczeń połączonych przejściami. Możesz je przeglądać, wprowadzając polecenia takie jak „idź na północ”.

Taka mapa w pełni odpowiada definicji skończonej maszyny stanów. Pokój, w którym się znajdujesz, jest stanem obecnym. Każde wyjście z pokoju jest przejściem. Polecenia nawigacyjne - wejście.

Wyliczenia i przełączniki

Jednym z problemów związanych z naszą starą klasą Heroine jest to, że pozwala ona na niepoprawną kombinację kluczy boolowskich: isJumping_ i isDucking_, nie mogą one być jednocześnie prawdziwe. A jeśli masz kilka flag boolowskich, z których tylko jedna może być prawdziwa, czy nie byłoby lepiej zastąpić je wszystkie enum .

W naszym przypadku za pomocą enum możemy całkowicie opisać wszystkie stany naszego FSM w ten sposób:

wyliczenie Stan ( STATE_STANDING, STATE_JUMPING, STATE_DUCKING, STATE_DIVING );

Zamiast kilku flag, Heroine ma tylko jedno pole state_. Będziemy musieli także zmienić kolejność rozgałęzień. W poprzednim przykładzie kodu rozgałęziliśmy się najpierw w zależności od danych wejściowych, a następnie od stanu. Robiąc to, pogrupowaliśmy kod według wciśniętego przycisku, ale zamazaliśmy kod powiązany ze stanami. Teraz zrobimy odwrotnie i przełączymy wejście w zależności od stanu. Oto co otrzymujemy:

void Heroine::handleInput(Wejście wejściowe) ( przełącznik (stan_) ( case STATE_STANDING: if (input == PRESS_B) ( stan_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) else if (input == PRESS_DOWN) ( stan_ = STATE_DUCKING; setGraphics(IMAGE_DUCK); ) przerwa ; przypadek STATE_JUMPING: if (input == PRESS_DOWN) ( stan_ = STATE_DIVING; setGraphics(IMAGE_DIVE); ) przerwa ; przypadek STATE_DUCKING: if (input == RELEASE_DOWN) ( stan_ = STATE_STANDING; setGraphics (IMAGE_STAND); ) przerwa ; ) )

Wygląda to dość banalnie, ale mimo to ten kod jest już znacznie lepszy od poprzedniego. Nadal mamy pewne rozgałęzienia warunkowe, ale uprościliśmy stan zmienny do pojedynczego pola. Cały kod zarządzający jednym stanem jest zebrany w jednym miejscu. Jest to najprostszy sposób implementacji maszyny o skończonych stanach i czasami jest wystarczający.

Teraz bohaterka nie będzie już mogła wejść niepewny stan : schorzenie. Podczas używania flag logicznych niektóre kombinacje były możliwe, ale nie miały sensu. Podczas korzystania z wyliczenia wszystkie wartości są poprawne.

Niestety Twój problem może przerosnąć to rozwiązanie. Powiedzmy, że chcemy dodać naszej bohaterce atak specjalny, do którego bohaterka musi usiąść, aby się naładować, a następnie rozładować zgromadzoną energię. A siedząc musimy zwracać uwagę na czas ładowania.

Dodaj pole ładowaniaTime_ do Heroine, aby przechowywać czas ładowania. Załóżmy, że mamy już metodę update() wywoływaną w każdej klatce. Dodajmy do niego następujący kod:

void Heroine::update() ( if (state_ == STATE_DUCKING) ( ChargeTime_++; if (chargeTime_ > MAX_CHARGE) ( superBomb(); ) ) )

Jeśli odgadłeś wzór metody aktualizacji, wygrałeś nagrodę!

Za każdym razem, gdy ponownie wykonujemy przysiad, musimy zresetować ten licznik czasu. Aby to zrobić, musimy zmienić handleInput() :

void Heroine::handleInput(Wejście wejściowe) ( przełącznik (stan_) ( case STATE_STANDING: if (input == PRESS_DOWN) ( stan_ = STATE_DUCKING; ChargeTime_ = 0 ; setGraphics(IMAGE_DUCK); ) // Przetwórz pozostałe dane wejściowe... przerwa ; // Inne stany... } }

Ostatecznie, aby dodać ten atak szarżowy, musieliśmy zmienić dwie metody i dodać pole ładowaniaTime_ do Heroine, mimo że jest ono używane tylko w stanie kucanym. Chciałbym mieć cały ten kod i dane w jednym miejscu. Gang Czterech może nam w tym pomóc.

Stan szablonu

Dla osób dobrze zorientowanych w paradygmacie obiektowym każda gałąź warunkowa jest okazją do wykorzystania dynamicznej wysyłki (innymi słowy wywołania metody wirtualnej w C++). Myślę, że musimy zejść jeszcze głębiej w tę króliczą norę. Czasami jeśli to wszystko, czego potrzebujemy.

Ma to uzasadnienie historyczne. Wielu starych apostołów paradygmatu obiektowego, takich jak Gang Czterech i ich zwolennicy Wzorce programowania i Martin Fuller ze swoim Refaktoryzacja pochodzi ze Smalltalka. I tam ifThen jest po prostu metodą, której używasz do przetwarzania warunku i która jest implementowana inaczej dla obiektów prawdziwych i fałszywych.

W naszym przykładzie dotarliśmy już do punktu krytycznego, w którym powinniśmy zwrócić uwagę na coś obiektowego. To prowadzi nas do wzorca państwa. Cytując Gang Czterech:

Pozwala obiektom zmieniać swoje zachowanie zgodnie ze zmianami stanu wewnętrznego. W takim przypadku obiekt będzie zachowywał się jak inna klasa.

Nie jest to zbyt jasne. W końcu Switch też sobie z tym radzi. W nawiązaniu do naszego przykładu z bohaterką szablon wyglądałby tak:

Interfejs stanu

Najpierw zdefiniujmy interfejs dla stanu. Każdy element zachowania zależnego od stanu – tj. wszystko, co wcześniej zaimplementowaliśmy za pomocą przełącznika, zamienia się w wirtualną metodę tego interfejsu. W naszym przypadku są to handleInput() i update() .

klasa HeroineState ( public : wirtualna ~HeroineState() () wirtualne wejście pustego uchwytu{} {} };

Zajęcia dla każdego stanu

Dla każdego stanu definiujemy klasę implementującą interfejs. Jego metody determinują zachowanie bohaterki w tym stanie. Innymi słowy, bierzemy wszystkie opcje z przełącznika z poprzedniego przykładu i zamieniamy je w klasę stanu. Na przykład:

klasa DuckingState: public HeroineState ( public : DuckingState() : ładowaniaTime_(0 ) () wirtualne wejście pustego uchwytu (Bohaterka i bohaterka, wejście wejściowe)( if (wejście == RELEASE_DOWN) ( // Przejście do stanu stałego... bohaterka.setGraphics(IMAGE_STAND); ) ) aktualizacja wirtualnej pustki (bohaterka i bohaterka)( ładowaniaCzas_++; if (Czas ładowania_ > MAX_CHARGE) ( bohaterka.superBomb(); ) ) prywatny: int opłataCzas_; );

Należy pamiętać, że przenieśliśmy ChargeTime_ z własnej klasy bohaterki do klasy DuckingState. I to bardzo dobrze, bo ta informacja ma znaczenie tylko w tym stanie i nasz model danych wyraźnie to wskazuje.

Delegacja do stanu

klasa Bohaterka (publiczna: wirtualne puste uchwytInput (wejście wejściowe)( stan_->uchwytInput(*to, wejście); ) aktualizacja wirtualnej pustki()( stan_->update(*this ); ) // Inne metody... prywatny: HeroineState* stan_; );

Aby „zmienić stan”, wystarczy, że stan_ będzie wskazywał inny obiekt HeroineState. Z tego właśnie składa się wzorzec Stanu.

Wygląda całkiem podobnie do szablonów strategii i obiektów typu GoF. We wszystkich trzech mamy obiekt główny delegujący do obiektu podrzędnego. Różnica jest zamiar.

  • Celem Strategii jest zmniejszenie łączności(oddzielenie) pomiędzy klasą główną i jej zachowaniem.
  • Celem obiektu typu jest utworzenie wielu obiektów, które zachowują się tak samo, dzieląc między sobą wspólny obiekt typu.
  • Celem państwa jest zmiana zachowania głównego obiektu poprzez zmianę obiektu, do którego deleguje.

Gdzie są te obiekty stanu?

Jest coś, o czym ci nie powiedziałem. Aby zmienić stan musimy przypisać state_ nową wartość wskazującą na nowy stan, ale skąd pochodzi ten obiekt? W naszym przykładzie wyliczeniowym nie ma się nad czym zastanawiać: wartości wyliczeniowe są po prostu prymitywami, takimi jak liczby. Ale teraz nasze stany są reprezentowane przez klasy, co oznacza, że ​​potrzebujemy wskaźników do rzeczywistych instancji. Istnieją dwie najczęstsze odpowiedzi:

Stany statyczne

Jeśli obiekt stanu nie ma innych pól, jedyną rzeczą, którą przechowuje, jest wskaźnik do wewnętrznej wirtualnej tabeli metod, dzięki czemu można wywołać te metody. W tym przypadku nie ma potrzeby posiadania więcej niż jednej instancji klasy: każda instancja będzie nadal taka sama.

Jeśli Twój stan nie ma pól i ma tylko jedną metodę wirtualną, możesz jeszcze bardziej uprościć wzorzec. Wymienimy każdego Klasa państwo funkcjonować stan - zwykła funkcja najwyższego poziomu. I odpowiednio pole państwo_ w naszej głównej klasie zamieni się w prosty wskaźnik funkcji.

Całkiem możliwe, że poradzi sobie tylko z jednym statyczny Kopiuj. Nawet jeśli masz całą masę FSM w tym samym stanie w tym samym czasie, wszystkie mogą wskazywać na tę samą instancję statyczną, ponieważ nie ma w tym nic specyficznego dla maszyny stanowej.

Miejsce umieszczenia instancji statycznej zależy od Ciebie. Znajdź miejsce, w którym będzie to odpowiednie. Umieśćmy naszą instancję w klasie bazowej. Bez powodu.

klasa HeroineState ( public : statyczny StandingState stojący; statyczny DuckingState pochylający się; statyczny JumpingState skoki; statyczny DivingState dive; //Reszta kodu... };

Każde z tych pól statycznych jest instancją stanu używanego przez grę. Aby bohaterka podskoczyła, stan stojący wykona coś takiego:

if (input == PRESS_B) ( heroine.state_ = &HeroineState::jumping; heroine.setGraphics(IMAGE_JUMP); )

Instancje państwowe

Czasami poprzednia opcja nie działa. Stan statyczny nie jest odpowiedni dla stanu przykucniętego. Posiada pole ładowaniaTime_ i jest specyficzne dla bohaterki, która będzie kucać. W naszym przypadku sprawdzi się to jeszcze lepiej, bo mamy tylko jedną bohaterkę, ale jeśli będziemy chcieli dodać kooperację dla dwóch graczy, będziemy mieli duże problemy.

W takim przypadku powinniśmy stworzyć obiekt stanu, gdy się do niego przejdziemy. Umożliwi to każdemu FSM posiadanie własnej instancji stanu. Oczywiście, jeśli przydzielimy pamięć na nowy warunek, oznacza to, że powinniśmy uwolnienie zajęta pamięć o bieżącym. Musimy zachować ostrożność, ponieważ kod powodujący zmiany znajduje się w bieżącym stanie metody. Nie chcemy tego sami usuwać spod spodu.

Zamiast tego pozwolimy funkcji handleInput() na HeroineState opcjonalnie zwrócić nowy stan. Gdy tak się stanie, Heroine usunie stary stan i zastąpi go nowym, w następujący sposób:

void Heroine::handleInput(Wejście wejściowe) ( HeroineState* stan = stan_->handleInput(*to, wejście); if (stan != NULL ) (usuń stan_; stan_ = stan; ) )

W ten sposób nie usuniemy poprzedniego stanu, dopóki nie wrócimy z naszej metody. Teraz stan stojący może przejść do stanu nurkowania, tworząc nową instancję:

HeroineState* StandingState::handleInput(Heroine& heroine, Wejście wejściowe) ( if (input == PRESS_DOWN) ( // Inny kod... return new DuckingState(); ) // Pozostań w tym stanie. return NULL ; )

Kiedy tylko mogę, wolę używać stanów statycznych, ponieważ nie zajmują one cykli pamięci i procesora poprzez alokację obiektów za każdym razem, gdy zmienia się stan. Dla warunków, które są czymś więcej niż tylko państwo- właśnie tego potrzebujesz.

Oczywiście, gdy dynamicznie alokujesz pamięć dla stanu, powinieneś pomyśleć o możliwej fragmentacji pamięci. Szablon puli obiektów może być pomocny.

Kroki logowania i wylogowywania

Wzorzec State ma na celu hermetyzację wszystkich zachowań i powiązanych danych w ramach jednej klasy. Radzimy sobie całkiem nieźle, ale wciąż są pewne niejasne szczegóły.

Kiedy bohaterka zmienia stan, zmieniamy także jej duszka. W tej chwili ten kod należy do stanu, z kogo ona przełącza. Kiedy stan przechodzi z nurkowania do pozycji stojącej, nurkowanie ustanawia swój obraz:

HeroineState* DuckingState::handleInput(Heroine& heroine, Wejście wejściowe) ( if (input == RELEASE_DOWN) ( heroine.setGraphics(IMAGE_STAND); zwróć nowy StandingState(); ) // Inny kod... )

Tak naprawdę chcemy, aby każdy stan kontrolował swoją własną grafikę. Możemy to osiągnąć dodając do stanu akcja wejściowa (akcja wejścia):

klasa StandingState: public HeroineState ( public : wejście do wirtualnej pustki (bohaterka i bohaterka)( heroine.setGraphics(IMAGE_STAND); ) // Inny kod... );

Wracając do Heroine, modyfikujemy kod tak, aby zmianie stanu towarzyszyło wywołanie funkcji wejściowej nowego stanu:

void Heroine::handleInput(Wejście wejściowe) ( HeroineState* stan = stan_->handleInput(*this , wejście); if (stan != NULL ) ( usuń stan_; stan_ = stan; // Wywołaj akcję wejściową nowego stanu. stan_->enter(*this ); ) )

Uprości to kod DuckingState:

HeroineState* DuckingState::handleInput(Heroine& heroine, Wejście wejściowe) ( if (input == RELEASE_DOWN) (zwróć nowy StandingState(); ) // Inny kod... )

Wszystko to polega na przełączeniu na pozycję stojącą, a stan stojący zajmuje się grafiką. Teraz nasze państwa są naprawdę zamknięte. Kolejną miłą cechą takiej akcji wejściowej jest to, że jest ona wyzwalana po wejściu w stan, niezależnie od stanu, w którym się znajduje Który byliśmy tam.

Większość wykresów stanów rzeczywistych ma wielokrotne przejścia do tego samego stanu. Przykładowo nasza bohaterka może strzelać z broni stojąc, siedząc czy skacząc. Oznacza to, że wszędzie tam, gdzie to nastąpi, możemy mieć do czynienia z powielaniem kodu. Akcja wejściowa pozwala zebrać je w jednym miejscu.

Można to zrobić przez analogię akcja wyjściowa (akcja wyjścia). Będzie to po prostu metoda, którą będziemy odwoływać się do stanu wcześniej odjazd go i przejść do nowego stanu.

I co osiągnęliśmy?

Spędziłem tyle czasu sprzedając ci FSM, a teraz mam zamiar wyciągnąć spod ciebie dywanik. Wszystko, co powiedziałem do tej pory, jest prawdą i stanowi świetne rozwiązanie problemu. Ale tak się składa, że ​​najważniejsze zalety maszyn skończonych są jednocześnie ich największymi wadami.

Maszyna stanów pomaga poważnie rozwikłać kod, organizując go w bardzo ścisłą strukturę. Wszystko, co mamy, to ustalony zestaw stanów, pojedynczy stan bieżący i zakodowane na stałe przejścia.

Automat skończony nie jest kompletny według Turinga. Teoria automatów opisuje kompletność za pomocą serii abstrakcyjnych modeli, każdy bardziej złożony od poprzedniego. Maszyna Turinga jest jedną z najbardziej wyrazistych.

„Ukończony Turing” oznacza system (zwykle język programowania), który jest wystarczająco wyrazisty, aby zaimplementować maszynę Turinga. To z kolei oznacza, że ​​wszystkie kompletne języki Turinga są w przybliżeniu równie wyraziste. FSM nie są na tyle wyraziści, aby wejść do tego klubu.

Jeśli spróbujesz użyć maszyny stanów do czegoś bardziej złożonego, na przykład sztucznej inteligencji w grach, natychmiast natkniesz się na ograniczenia tego modelu. Na szczęście nasi poprzednicy nauczyli się omijać pewne przeszkody. Zakończę ten rozdział kilkoma takimi przykładami.

Konkurencyjna maszyna stanowa

Postanowiliśmy dodać dla naszej bohaterki możliwość noszenia broni. Chociaż jest teraz uzbrojona, nadal może robić wszystko, co mogła robić wcześniej: biegać, skakać, kucać itp. Ale teraz, robiąc to wszystko, może także strzelać z broni.

Jeśli chcemy dopasować to zachowanie do ram FSM, będziemy musieli podwoić liczbę stanów. Dla każdego ze stanów będziemy musieli stworzyć inny taki sam, ale dla bohaterki z bronią: stanie, stanie z bronią, skakanie, skakanie z bronią... No cóż, rozumiesz.

Jeśli dodasz jeszcze kilka broni, liczba stanów wzrośnie kombinatorycznie. I to nie jest tylko zbiór stanów, ale także splot powtórzeń: stan uzbrojony i nieuzbrojony są prawie identyczne, z wyjątkiem części kodu odpowiedzialnej za strzelanie.

Problem w tym, że mylimy dwie części państwa – czyli ono robi Więc co trzyma w rękach- w jednej maszynie. Aby wymodelować wszystkie możliwe kombinacje, musimy stworzyć dla każdej stan pary. Rozwiązanie jest oczywiste: musisz utworzyć dwie oddzielne maszyny stanowe.

Jeśli chcemy się zjednoczyć N stany działania i M stany tego, co trzymamy w dłoniach, w jedną skończoną maszynę stanów - potrzebujemy n×m stwierdza. Jeśli mamy dwa karabiny maszynowe, będziemy potrzebować n+m stwierdza.

Naszą pierwszą maszynę stanów pozostawimy bez zmian. A oprócz tego stworzymy kolejną maszynę opisującą to, co trzyma bohaterka. Teraz Heroine będzie miała dwa odniesienia do „stanu”, po jednym dla każdej maszyny.

klasa Bohaterka ( //Reszta kodu... prywatny: HeroineState* stan_; HeroineState* wyposażenie_; );

Dla ilustracji używamy pełnej implementacji wzorca State dla drugiej maszyny stanu, chociaż w praktyce w tym przypadku wystarczyłaby prosta flaga Boole'a.

Kiedy bohaterka deleguje dane wejściowe do stanów, przekazuje tłumaczenie do obu maszyn stanowych:

void Heroine::handleInput(Wejście wejściowe) ( stan_->handleInput(*to, wejście); equipment_->handleInput(*to, wejście); )

Bardziej złożone systemy mogą obejmować maszyny o skończonych stanach, które mogą wchłonąć część danych wejściowych, tak że inne maszyny nie będą już ich otrzymywać. Dzięki temu zapobiegniemy sytuacji, w której kilka maszyn odpowiada na to samo wejście.

Każda maszyna stanowa może reagować na dane wejściowe, wywoływać zachowanie i zmieniać swój stan niezależnie od innych maszyn stanowych. A gdy oba stany są praktycznie niepowiązane, sprawdza się świetnie.

W praktyce można spotkać się z sytuacją, w której państwa oddziałują na siebie. Nie może np. strzelać podczas skoku czy np. wykonywać ataku wślizgiem, gdy jest uzbrojona. Aby zapewnić takie zachowanie i koordynację automatów w kodzie, będziesz musiał wrócić do tej samej kontroli brutalnej siły za pomocą if inny maszyna skończona. Nie jest to najbardziej eleganckie rozwiązanie, ale przynajmniej działa.

Hierarchiczna maszyna stanu

Po dalszej rewitalizacji zachowań bohaterki zapewne będzie ona miała całą masę podobnych stanów. Na przykład nie może wystąpić stanie, chodzenie, bieganie i zjeżdżanie po zboczu. W każdym z tych stanów naciśnięcie B powoduje podskoczenie, a naciśnięcie powoduje kucanie.

W najprostszej implementacji maszyny stanowej zduplikowaliśmy ten kod dla wszystkich stanów. Ale oczywiście byłoby znacznie lepiej, gdybyśmy musieli napisać kod tylko raz, a następnie moglibyśmy go ponownie wykorzystać dla wszystkich stanów.

Gdyby był to tylko kod obiektowy, a nie maszyna stanowa, moglibyśmy zastosować technikę oddzielania kodu między stanami zwaną dziedziczeniem. Możesz zdefiniować klasę dla stanu podstawowego, która będzie obsługiwać skakanie i kucanie. Stanie, chodzenie, bieganie i toczenie się są dla niego dziedziczone i dodają własne dodatkowe zachowanie.

Ta decyzja ma zarówno dobre, jak i złe konsekwencje. Dziedziczenie jest potężnym narzędziem do ponownego wykorzystania kodu, ale jednocześnie zapewnia bardzo silną spójność pomiędzy dwoma fragmentami kodu. Młot jest zbyt ciężki, aby uderzać go bezmyślnie.

W tej formie wynikowa struktura zostanie wywołana hierarchiczna maszyna stanu(Lub automat hierarchiczny). A każdy warunek może mieć swój własny superpaństwo(sam stan nazywa się podstan). Kiedy zdarzenie ma miejsce, a podstan go nie przetwarza, zostaje ono przekazane dalej w łańcuchu superstanów. Innymi słowy, wygląda to na przesłonięcie odziedziczonej metody.

W rzeczywistości, jeśli użyjemy oryginalnego wzorca State do implementacji FSM, możemy już użyć dziedziczenia klas do implementacji hierarchii. Zdefiniujmy klasę bazową dla nadklasy:

klasa OnGroundState: public HeroineState ( public : wirtualne wejście pustego uchwytu (Bohaterka i bohaterka, wejście wejściowe)( if (input == PRESS_B) ( // Skok... ) else if (input == PRESS_DOWN) ( // Przysiad... ) ) );

A teraz każda podklasa odziedziczy to:

klasa DuckingState: public OnGroundState (public: wirtualne wejście pustego uchwytu (Bohaterka i bohaterka, wejście wejściowe)( if (input == RELEASE_DOWN) ( // Wstań... ) else ( // Dane wejściowe nie są przetwarzane. Dlatego przekazujemy go wyżej w hierarchii. OnGroundState::handleInput(bohaterka, wejście); ) ) );

Oczywiście nie jest to jedyny sposób na wdrożenie hierarchii. Jeśli jednak nie użyjesz szablonu Gang Czterech Państw, nie zadziała. Zamiast tego możesz modelować przejrzystą hierarchię bieżących stanów i superstanów za pomocą stos stany zamiast pojedynczego stanu w klasie głównej.

Bieżący stan będzie na górze stosu, poniżej będzie jego superstan, a następnie superstan Ten superpaństwa itp. A kiedy musisz zaimplementować zachowanie specyficzne dla stanu, zaczynasz od szczytu stosu i schodzisz w dół, aż stan sobie z tym poradzi. (A jeśli tego nie przetworzy, po prostu to zignorujesz).

Automat z pamięcią magazynka

Istnieje inne popularne rozszerzenie maszyn stanowych, które również korzysta ze stosu stanu. Tylko tutaj stos reprezentuje zupełnie inną koncepcję i służy do rozwiązywania różnych problemów.

Problem polega na tym, że maszyna stanów nie ma koncepcji historie. Czy wiesz w jakim jesteś stanie? jesteś, ale nie masz informacji o tym, w jakim stanie się znajdujesz był. W związku z tym nie ma łatwego sposobu na powrót do poprzedniego stanu.

Oto prosty przykład: wcześniej pozwalaliśmy naszej nieustraszonej bohaterce uzbroić się po zęby. Kiedy strzela z broni, potrzebujemy nowego stanu, aby odtwarzać animację strzału, pojawiać się pocisk i towarzyszące mu efekty wizualne. W tym celu tworzymy nowy FiringState i dokonujemy do niego przejść ze wszystkich stanów, w których bohaterka może strzelać wciskając przycisk strzału.

Ponieważ to zachowanie jest powielane w wielu stanach, w tym miejscu można użyć hierarchicznej maszyny stanów do ponownego użycia kodu.

Trudność polega na tym, że musisz w jakiś sposób zrozumieć, do jakiego stanu musisz przejść. Po strzelanie. Bohaterka może wystrzelić cały magazynek stojąc w miejscu, biegając, skacząc czy kucając. Po zakończeniu sekwencji strzelań musi powrócić do stanu, w jakim znajdowała się przed oddaniem strzału.

Jeśli przywiążemy się do czystego FSM, natychmiast zapominamy, w jakim stanie się znajdowaliśmy. Aby to śledzić, musimy zdefiniować wiele niemal identycznych stanów - strzelanie z pozycji stojącej, strzelanie z biegu, strzelanie z wyskoku itp. W ten sposób mamy zakodowane na stałe przejścia, które po zakończeniu przechodzą do prawidłowego stanu.

Tak naprawdę potrzebujemy możliwości zapamiętania stanu, w jakim byliśmy przed strzelaniną i zapamiętania go ponownie po strzelaninie. Tutaj znowu może nam pomóc teoria automatów. Odpowiednia struktura danych nazywa się Automatem Pushdown.

Tam, gdzie w automacie skończonym mamy pojedynczy wskaźnik do stanu, w automacie ze skończoną pamięcią jest ich stos. W FSM przejście do nowego stanu zastępuje poprzedni. Maszyna z pamięcią magazynkową również pozwala to zrobić, ale dodaje jeszcze dwie operacje:

    Możesz miejsce (naciskać) nowy stan na stosie. Bieżący stan zawsze będzie na górze stosu, jest to więc operacja przejścia do nowego stanu. Ale jednocześnie stary stan pozostaje bezpośrednio pod obecnym na stosie i nie znika bez śladu.

    Możesz wyciąg (Muzyka pop) najwyższy stan ze stosu. Stan znika, a to, co było pod nim, staje się aktualne.

To wszystko, czego potrzebujemy do strzelania. Tworzymy Jedyną rzeczą stan strzelania. Kiedy naciśniemy przycisk wyzwalania w innym stanie, nastąpi to miejsce (naciskać) stan strzelania stosem. Kiedy animacja strzelania się zakończy, my wyciąg (Muzyka pop) stan i maszyna z pamięcią magazynka automatycznie przywraca nas do poprzedniego stanu.

Jak bardzo są one naprawdę przydatne?

Nawet przy tej ekspansji maszyn stanowych ich możliwości są nadal dość ograniczone. W dzisiejszej AI dominuje trend polegający na używaniu takich rzeczy jak drzewa zachowań(drzewa zachowań) i systemy planowania(systemy planowania). A jeśli szczególnie interesuje Cię dziedzina sztucznej inteligencji, cały ten rozdział powinien zaostrzyć Twój apetyt. Aby go zadowolić, będziesz musiał sięgnąć do innych książek.

Nie oznacza to wcale, że maszyny o skończonych stanach, maszyny z pamięcią magazynową i inne podobne systemy są całkowicie bezużyteczne. W niektórych przypadkach są to dobre narzędzia do modelowania. Maszyny stanowe są przydatne, gdy:

  • Masz jednostkę, której zachowanie zmienia się w zależności od jej stanu wewnętrznego.
  • Warunek ten jest ściśle podzielony na stosunkowo niewielką liczbę konkretnych opcji.
  • Jednostka stale reaguje na serię poleceń wejściowych lub zdarzeń.

W grach maszyny stanowe są zwykle używane do modelowania sztucznej inteligencji, ale można ich również używać do implementowania danych wprowadzanych przez użytkownika, nawigacji w menu, analizowania tekstu, protokołów sieciowych i innych zachowań asynchronicznych.

"WzórPaństwo"źródło.ru

Stan to wzorzec zachowania obiektu, który określa różną funkcjonalność w zależności od wewnętrznego stanu obiektu. witryna internetowa źródło witryny internetowej oryginał

Warunki, zadanie, cel

Pozwala obiektowi zmieniać swoje zachowanie w zależności od jego stanu wewnętrznego. Ponieważ zachowanie może zmieniać się całkowicie dowolnie i bez żadnych ograniczeń, z zewnątrz wydaje się, że zmieniła się klasa obiektu.

Motywacja

Weź pod uwagę klasę Połączenie TCP, który reprezentuje połączenie sieciowe. Obiekt tej klasy może znajdować się w jednym z kilku stanów: Przyjęty(zainstalowany), Słuchający(słuchający), Zamknięte(Zamknięte). Kiedy obiekt Połączenie TCP otrzymuje żądania od innych obiektów, reaguje różnie w zależności od aktualnego stanu. Na przykład odpowiedź na żądanie otwarty(otwarte) zależy od tego, czy połączenie jest w stanie Zamknięte Lub Przyjęty. Wzorzec stanu opisuje sposób działania obiektu Połączenie TCP może zachowywać się inaczej w różnych stanach. źródło witryny oryginalna witryna

Główną ideą tego wzorca jest wprowadzenie klasy abstrakcyjnej Stan TCP do reprezentowania różnych stanów połączenia. Ta klasa deklaruje interfejs wspólny dla wszystkich klas opisujących różnych procesów roboczych. oryginalne źródło.ru

stan : schorzenie. W tych podklasach Stan TCP Zaimplementowane jest zachowanie specyficzne dla stanu. Na przykład na zajęciach TCPUstalono I TCPZamknięte zaimplementowano zachowanie specyficzne dla stanu Przyjęty I Zamknięte odpowiednio. oryginalne źródło witryny internetowej

oryginał.ru

Klasa Połączenie TCP przechowuje obiekt stanu (instancję podklasy Stan TCP) reprezentujący bieżący stan połączenia i deleguje wszystkie żądania zależne od stanu do tego obiektu. Połączenie TCP używa własnej instancji podklasy Stan TCP całkiem proste: wywoływanie metod pojedynczego interfejsu Stan TCP, tylko w zależności od tego, jaka konkretna podklasa jest aktualnie przechowywana Stan TCP-a - wynik jest inny, tj. w rzeczywistości wykonywane są operacje specyficzne tylko dla tego stanu połączenia. źródło: Original.ru

I za każdym razem, gdy zmienia się stan połączeniaPołączenie TCP zmienia swój obiekt stanu. Na przykład, gdy nawiązane połączenie zostanie zamknięte, Połączenie TCP zastępuje instancję klasy TCPUstalono Kopiuj TCPZamknięte. oryginalna witryna źródłowa

Znaki zastosowania, użycie wzorca Stan

Użyj wzorca stanu w następujących przypadkach: source.ru
  1. Kiedy zachowanie obiektu zależy od jego stanu i musi się zmienić w czasie wykonywania. źródło oryginał.ru
  2. Gdy kod operacji zawiera instrukcje warunkowe składające się z wielu gałęzi, w których wybór gałęzi zależy od stanu. Zwykle w tym przypadku stan jest reprezentowany przez wyliczone stałe. Często ta sama struktura instrukcji warunkowej powtarza się w kilku operacjach.Wzorzec stanu sugeruje umieszczenie każdej gałęzi w osobnej klasie. Pozwala to traktować stan obiektu jako niezależny obiekt, który może zmieniać się niezależnie od innych. źródło witryny oryginalna witryna

Rozwiązanie

oryginalne źródło witryny internetowej

źródło.ru

Uczestnicy wzorca Państwa

źródło.ru
  1. Kontekst(Połączenie TCP) - kontekst.
    Definiuje pojedynczy interfejs dla klientów.
    Przechowuje instancję podklasy Stan betonu, który określa aktualny stan. zajęcia z kodowania
  2. Państwo(TCPState) - stan.
    Definiuje interfejs do hermetyzacji zachowania związanego z określonym stanem kontekstu. źródło witryny oryginał witryny
  3. Podklasy Stan betonu(TCPEstablished, TCPListen, TCPClosed) - konkretny stan.
    Każda podklasa implementuje zachowanie powiązane z pewnym stanem kontekstu Kontekst. oryginalna witryna źródłowa

Schemat wykorzystania wzorca State

Klasa Kontekst deleguje żądania do bieżącego obiektu Stan betonu. oryginalna witryna źródłowa

Kontekst może przekazać sam siebie jako argument do obiektu Państwo, który przetworzy żądanie. Dzięki temu obiekt stanu ( Stan betonu) w razie potrzeby uzyskaj dostęp do kontekstu. zajęcia z kodowania

Kontekst- To jest główny interfejs dla klientów. Klienci mogą konfigurować kontekst za pomocą obiektów stanu Państwo(dokładniej Stan betonu). Po skonfigurowaniu kontekstu klienci nie muszą już komunikować się bezpośrednio z obiektami stanu (tylko za pośrednictwem wspólnego interfejsu Państwo). źródło.ru

W tym przypadku też Kontekst lub same podklasy Stan betonu może decydować, pod jakimi warunkami i w jakiej kolejności następuje zmiana stanów. witryna internetowa źródło witryny internetowej oryginał

Pytania dotyczące implementacji wzorca Państwa

Pytania dotyczące implementacji wzorca State: źródło oryginał.ru
  1. Co decyduje o przejściach pomiędzy stanami.
    Wzorzec stanu nie mówi nic o tym, który uczestnik określa warunki (kryteria) przejścia między stanami. Jeśli kryteria są stałe, można je wdrożyć bezpośrednio w klasie Kontekst. Jednak ogólnie rzecz biorąc, bardziej elastycznym i poprawnym podejściem jest zezwolenie na podklasy samych klas Państwo określić kolejny stan i moment przejścia. Aby to zrobić na zajęciach Kontekst musimy dodać interfejs, który pozwala na korzystanie z obiektów Państwo ustawić jego stan.
    Tę zdecentralizowaną logikę przejścia można łatwiej modyfikować i rozszerzać — wystarczy zdefiniować nowe podklasy Państwo. Wadą decentralizacji jest to, że każda podklasa Państwo musi „wiedzieć” o przynajmniej jednej podklasie innego stanu (do której faktycznie może przełączyć stan bieżący), co wprowadza zależności implementacyjne pomiędzy podklasami. źródło witryny oryginał witryny

    źródło oryginał.ru
  2. Alternatywa tabelaryczna.
    Istnieje inny sposób konstruowania kodu sterowanego stanem. Jest to zasada skończonej maszyny stanów. Wykorzystuje tabelę do mapowania wejść na przejścia stanów. Za jego pomocą można określić, w jaki stan należy przejść po nadejściu określonych danych wejściowych. Zasadniczo zastępujemy kod warunkowy wyszukiwaniem w tabeli.
    Główną zaletą maszyny jest jej regularność: aby zmienić kryteria przejścia, wystarczy zmodyfikować tylko dane, a nie kod. Ale są też wady:
    - przeszukiwanie tabeli jest często mniej efektywne niż wywołanie funkcji,
    - przedstawienie logiki przejścia w jednolitej formie tabelarycznej sprawia, że ​​kryteria są mniej jednoznaczne i przez to trudniejsze do zrozumienia,
    - zwykle trudno jest dodać akcje towarzyszące przejściom pomiędzy stanami. Metoda tabelaryczna uwzględnia stany i przejścia między nimi, wymaga jednak uzupełnienia, aby przy każdej zmianie stanu można było dokonywać dowolnych obliczeń.
    Główną różnicę między maszynami stanów opartymi na tabelach a stanem wzorca można sformułować w następujący sposób: Stan wzorca modeluje zachowanie zależne od stanu, a metoda tabelaryczna skupia się na definiowaniu przejść między stanami. oryginał.ru

    oryginalna witryna źródłowa
  3. Tworzenie i niszczenie obiektów stanu.
    Podczas procesu programowania zwykle musisz wybierać pomiędzy:
    - tworzenie obiektów stanu wtedy, gdy są potrzebne i niszczenie ich bezpośrednio po użyciu,
    - tworząc je z góry i na zawsze.

    Pierwsza opcja jest preferowana, gdy nie wiadomo z góry, w jakie stany wpadnie system, a kontekst zmienia stan stosunkowo rzadko. Jednocześnie nie tworzymy obiektów, które nigdy nie zostaną wykorzystane, co jest ważne, jeśli w obiektach stanu przechowywanych jest dużo informacji. Kiedy zmiany stanu występują często i nie chcesz niszczyć reprezentujących je obiektów (ponieważ mogą wkrótce być ponownie potrzebne), powinieneś zastosować drugie podejście. Czas na tworzenie obiektów poświęcany jest tylko raz, na samym początku, a czasu na niszczenie nie poświęca się w ogóle. Co prawda takie podejście może okazać się niewygodne, gdyż w kontekście muszą znajdować się odniesienia do wszystkich stanów, w jakie teoretycznie mógłby wpaść system. źródło oryginał.ru

    oryginał.ru źródło
  4. Korzystanie z dynamicznej zmiany.
    Możliwe jest różnicowanie zachowania na żądanie poprzez zmianę klasy obiektu w czasie wykonywania, ale większość języków obiektowych tego nie obsługuje. Wyjątkiem jest Perl, JavaScript i inne języki oparte na silnikach skryptowych, które zapewniają taki mechanizm i dlatego bezpośrednio obsługują Pattern State. Dzięki temu obiekty mogą zmieniać swoje zachowanie poprzez zmianę kodu klasy. źródło.ru

    .ru źródło oryginalne

wyniki

Wyniki użycia stan wzorca: źródło: Original.ru
  1. Lokalizuje zachowanie zależne od stanu.
    I dzieli go na części odpowiadające stanom. Wzorzec stanu umieszcza całe zachowanie związane z określonym stanem w oddzielnym obiekcie. Ponieważ kod zależny od stanu jest w całości zawarty w jednej z podklas klasy Państwo, możesz dodać nowe stany i przejścia, po prostu tworząc nowe podklasy.
    Zamiast tego można użyć elementów danych do zdefiniowania stanów wewnętrznych, a następnie operacji obiektu Kontekst sprawdzi te dane. Jednak w tym przypadku podobne instrukcje warunkowe lub instrukcje rozgałęziające byłyby rozproszone po całym kodzie klasy Kontekst. Jednakże dodanie nowego stanu wymagałoby zmiany kilku operacji, co utrudniłoby konserwację. Wzorzec stanu rozwiązuje ten problem, ale tworzy także inny, ponieważ zachowanie dla różnych stanów zostaje rozłożone na kilka podklas Państwo. Zwiększa to liczbę zajęć. Oczywiście jedna klasa jest bardziej zwarta, ale jeśli jest wiele stanów, to taki rozkład jest bardziej efektywny, gdyż w przeciwnym razie trzeba by mieć do czynienia z uciążliwymi instrukcjami warunkowymi.
    Posiadanie uciążliwych instrukcji warunkowych jest niepożądane, podobnie jak długie procedury. Są zbyt monolityczne, dlatego modyfikowanie i rozszerzanie kodu staje się problemem. Wzorzec stanu oferuje lepszy sposób konstruowania kodu zależnego od stanu. Logika opisująca przejścia stanów nie jest już opakowana w monolityczne stwierdzenia Jeśli Lub przełącznik, ale rozdzielone pomiędzy podklasami Państwo. Hermetyzując każde przejście i akcję w klasę, stan staje się pełnoprawnym obiektem. Poprawia to strukturę kodu i czyni jego cel jaśniejszym. oryginał.ru
  2. Sprawia, że ​​przejścia między stanami są wyraźne.
    Jeśli obiekt definiuje swój aktualny stan wyłącznie w oparciu o dane wewnętrzne, to przejścia pomiędzy stanami nie mają wyraźnej reprezentacji; pojawiają się one jedynie jako przypisania do określonych zmiennych. Wprowadzenie oddzielnych obiektów dla różnych stanów sprawia, że ​​przejścia są bardziej wyraźne. Poza tym obiekty Państwo może chronić kontekst Kontekst z niedopasowania zmiennych wewnętrznych, ponieważ przejścia z punktu widzenia kontekstu są działaniami atomowymi. Aby dokonać przejścia należy zmienić wartość tylko jednej zmiennej (zmiennej obiektowej Państwo w klasie Kontekst), a nie kilka. źródło.ru
  3. Obiekty stanu można udostępniać.
    Jeśli w obiekcie stanu Państwo nie ma zmiennych instancji, co oznacza, że ​​stan, który reprezentuje, jest kodowany wyłącznie przez sam typ, wówczas różne konteksty mogą współużytkować ten sam obiekt Państwo. Kiedy państwa są rozdzielone w ten sposób, są one zasadniczo oportunistami (patrz wzór oportunistyczny), którzy nie mają stanu wewnętrznego, a jedynie zachowanie. źródło witryny oryginalna witryna

Przykład

Przyjrzyjmy się realizacji przykładu z sekcji „”, tj. budowanie prostej architektury połączeń TCP. Jest to uproszczona wersja protokołu TCP, nie reprezentuje ona oczywiście całego protokołu ani nawet wszystkich stanów połączeń TCP. oryginał.ru źródło

Na początek zdefiniujmy klasę Połączenie TCP, który zapewnia interfejs do przesyłania danych i obsługuje żądania zmiany stanu: TCPConnection. oryginał.ru źródło

W zmiennej składowej państwo klasa Połączenie TCP przechowywana jest instancja klasy Stan TCP. Ta klasa powiela interfejs zmiany stanu zdefiniowany w klasie Połączenie TCP. źródło: Original.ru

oryginalna witryna źródłowa

Połączenie TCP deleguje wszystkie żądania zależne od stanu do instancji przechowywanej w stanie Stan TCP. Również na zajęciach Połączenie TCP jest operacja Zmień stan, za pomocą którego możesz zapisać w tej zmiennej wskaźnik do innego obiektu Stan TCP. Konstruktor klasy Połączenie TCP inicjuje państwo wskaźnik do stanu zamkniętego TCPZamknięte(zdefiniujemy to poniżej). źródło oryginał.ru

oryginał.ru źródło

Każda operacja Stan TCP akceptuje instancję Połączenie TCP jako parametr, umożliwiając w ten sposób obiekt Stan TCP dostęp do danych obiektu Połączenie TCP i zmień stan połączenia. .ru

W klasie Stan TCP zaimplementowano domyślne zachowanie dla wszystkich delegowanych do niego żądań. Może także zmienić stan obiektu Połączenie TCP poprzez operację Zmień stan. Stan TCP znajduje się w tym samym pakiecie co Połączenie TCP, więc ma również dostęp do tej operacji: TCPState . oryginalna witryna źródłowa

oryginał.ru

W podklasach Stan TCP Zaimplementowano zachowanie zależne od stanu. Połączenie TCP może znajdować się w wielu stanach: Przyjęty(zainstalowany), Słuchający(słuchający), Zamknięte(zamknięte) itp., a każda z nich ma swoją własną podklasę Stan TCP. Dla uproszczenia rozważymy szczegółowo tylko 3 podklasy - TCPUstalono, TCPSłuchaj I TCPZamknięte. oryginalne źródło witryny internetowej

Źródło Ru

W podklasach Stan TCP implementuje zachowanie zależne od stanu dla tych żądań, które są ważne w tym stanie. .ru

oryginalna witryna źródłowa

Po wykonaniu działań specyficznych dla stanu, te operacje źródło oryginał.ru

przyczyna Zmień stan zmienić stan obiektu Połączenie TCP. On sam nie ma żadnych informacji na temat protokołu TCP. To podklasy Stan TCP zdefiniować przejścia pomiędzy stanami i działaniami podyktowanymi protokołem. oryginał.ru

źródło oryginał.ru

Znane zastosowania wzorca stanu

Ralph Johnson i Jonathan Zweig charakteryzują wzorzec stanu i opisują go w odniesieniu do protokołu TCP.
Najpopularniejsze interaktywne programy do rysowania zapewniają „narzędzia” do wykonywania operacji bezpośredniej manipulacji. Na przykład narzędzie do rysowania linii umożliwia użytkownikowi kliknięcie myszą w dowolnym punkcie, a następnie przesunięcie myszy w celu narysowania linii od tego punktu. Narzędzie zaznaczania umożliwia zaznaczanie niektórych kształtów. Zazwyczaj wszystkie dostępne narzędzia umieszczane są na palecie. Zadaniem użytkownika jest wybranie i zastosowanie narzędzia, ale w rzeczywistości zachowanie redaktora zmienia się wraz ze zmianą narzędzia: za pomocą narzędzia do rysowania tworzymy kształty, za pomocą narzędzia zaznaczania je zaznaczamy i tak dalej. oryginalne źródło witryny internetowej

Aby odzwierciedlić zależność zachowania edytora od bieżącego narzędzia, można skorzystać ze wzorca stanu. oryginalne źródło.ru

Można zdefiniować klasę abstrakcyjną Narzędzie, których podklasy implementują zachowanie specyficzne dla narzędzia. Edytor graficzny przechowuje łącze do bieżącego obiektu Zbytl i przekazuje mu przychodzące żądania. Po wybraniu narzędzia edytor używa innego obiektu, co powoduje zmianę zachowania. .ru

Technikę tę wykorzystuje się w ramach edytorów graficznych HotDraw i Unidraw. Umożliwia klientom łatwe definiowanie nowych typów narzędzi. W HotDraw Klasa Kontroler rysunków przekazuje żądania do bieżącego obiektu Narzędzie. W Unidraw nazywane są odpowiednie klasy Widz I Narzędzie. Poniższy diagram klas przedstawia schematyczną reprezentację interfejsów klas Narzędzie

oryginał.ru

Cel wzorca Stan

  • Wzorzec State pozwala obiektowi zmienić swoje zachowanie w zależności od jego stanu wewnętrznego. Wygląda na to, że obiekt zmienił klasę.
  • Wzorzec stanu jest zorientowaną obiektowo implementacją maszyny stanu.

Problem do rozwiązania

Zachowanie obiektu zależy od jego stanu i musi się zmieniać w trakcie wykonywania programu. Taki schemat można zrealizować wykorzystując wiele operatorów warunkowych: na podstawie analizy aktualnego stanu obiektu podejmowane są określone działania. Jednak przy dużej liczbie stanów instrukcje warunkowe będą rozproszone po całym kodzie i taki program będzie trudny w utrzymaniu.

Omówienie wzorca Państwa

Wzorzec State rozwiązuje ten problem w następujący sposób:

  • Wprowadza klasę Context, która definiuje interfejs do świata zewnętrznego.
  • Wprowadza abstrakcyjną klasę State.
  • Reprezentuje różne „stany” maszyny stanowej jako podklasy stanu.
  • Klasa Context posiada wskaźnik do bieżącego stanu, który zmienia się wraz ze zmianą stanu maszyny stanowej.

Wzorzec Stan nie definiuje, gdzie dokładnie jest określany warunek przejścia do nowego stanu. Istnieją dwie opcje: klasa Kontekst lub podklasy Stan. Zaletą tej drugiej opcji jest to, że można łatwo dodawać nowe klasy pochodne. Wadą jest to, że każda podklasa stanu musi wiedzieć o swoich sąsiadach, aby dokonać przejścia do nowego stanu, co wprowadza zależności pomiędzy podklasami.

Istnieje również alternatywne podejście do projektowania maszyn o skończonych stanach, oparte na tabelach, polegające na wykorzystaniu tabeli, która w unikalny sposób odwzorowuje dane wejściowe na przejścia między stanami. Jednak takie podejście ma wady: trudno jest dodać wykonanie akcji podczas wykonywania przejść. Podejście oparte na wzorcu stanu wykorzystuje kod (zamiast struktur danych) do wykonywania przejść między stanami, dzięki czemu te akcje można łatwo dodać.

Struktura wzorca stanu

Klasa Context definiuje zewnętrzny interfejs dla klientów i przechowuje odwołanie do bieżącego stanu obiektu State. Interfejs abstrakcyjnej klasy bazowej State jest taki sam jak interfejs Context z wyjątkiem jednego dodatkowego parametru – wskaźnika do instancji Context. Klasy pochodne stanu definiują zachowanie specyficzne dla stanu. Klasa opakowania Context deleguje wszystkie odebrane żądania do obiektu „bieżącego stanu”, który może użyć otrzymanego dodatkowego parametru w celu uzyskania dostępu do instancji Context.

Wzorzec State pozwala obiektowi zmienić swoje zachowanie w zależności od jego stanu wewnętrznego. Podobny obraz można zaobserwować w działaniu automatu. Maszyny mogą mieć różne stany w zależności od dostępności towaru, ilości otrzymanych monet, możliwości wymiany pieniędzy itp. Po dokonaniu przez kupującego wyboru i opłaceniu produktu możliwe są następujące sytuacje (stany):

  • Oddaj towar kupującemu, zmiana nie jest wymagana.
  • Daj kupującemu towar i zmień.
  • Kupujący nie otrzyma towaru ze względu na brak wystarczających środków pieniężnych.
  • Kupujący nie otrzyma towaru z powodu jego braku.

Korzystanie ze wzorca stanu

  • Zdefiniuj istniejącą lub utwórz nową klasę opakowania kontekstowego, która będzie używana przez klienta jako „maszyna stanu”.
  • Utwórz podstawową klasę State, która replikuje interfejs klasy Context. Każda metoda przyjmuje jeden dodatkowy parametr: instancję klasy Context. Klasa State może definiować dowolne przydatne zachowanie „domyślne”.
  • Utwórz klasy wywodzące się ze stanu dla wszystkich możliwych stanów.
  • Klasa opakowania Context zawiera odwołanie do obiektu bieżącego stanu.
  • Klasa Context po prostu deleguje wszystkie żądania otrzymane od klienta do obiektu „bieżącego stanu”, przekazując adres obiektu Context jako dodatkowy parametr.
  • Korzystając z tego adresu, metody klasy State mogą w razie potrzeby zmienić „bieżący stan” klasy Context.

Cechy wzorca Stan

  • Obiekty stanu są często singletonami.
  • Flyweight pokazuje, jak i kiedy można dzielić obiekty State.
  • Wzorzec interpretera może używać stanu do definiowania kontekstów analizowania.
  • Wzorce State i Bridge mają podobne struktury, z tą różnicą, że Bridge pozwala na hierarchię klas obwiedni (analogi klas „opakowujących”), podczas gdy State nie. Wzorce te mają podobne struktury, ale rozwiązują różne problemy: Stan pozwala obiektowi zmieniać swoje zachowanie w zależności od jego stanu wewnętrznego, natomiast Bridge oddziela abstrakcję od jej implementacji, dzięki czemu można je zmieniać niezależnie od siebie.
  • Implementacja wzorca Stan opiera się na wzorcu Strategia. Różnice tkwią w ich przeznaczeniu.

Implementacja wzorca State

Rozważmy przykład maszyny o skończonych stanach z dwoma możliwymi stanami i dwoma zdarzeniami.

#włączać używając przestrzeni nazw std; klasa Maszyna ( stan klasy *bieżący; publiczny: Maszyna(); void setCurrent(Stan *s) ( prąd = s; ) void on(); void off(); ); class State (public: virtual void on(Machine *m) ( cout<< " already ON\n"; } virtual void off(Machine *m) { cout << " already OFF\n"; } }; void Machine::on() { current->na to); ) void Machine::off() ( current->off(this); ) klasa ON: public State ( public: ON() ( cout<< " ON-ctor "; }; ~ON() { cout << " dtor-ON\n"; }; void off(Machine *m); }; class OFF: public State { public: OFF() { cout << " OFF-ctor "; }; ~OFF() { cout << " dtor-OFF\n"; }; void on(Machine *m) { cout << " going from OFF to ON"; m->setCurrent(nowe WŁ.()); Usuń to; ) ); void ON::off(Maszyna *m) ( cout<< " going from ON to OFF"; m->setCurrent(nowe WYŁ.()); Usuń to; ) Maszyna::Machine() ( bieżąca = nowa WYŁ.(); cout<< "\n"; } int main() { void(Machine:: *ptrs)() = { Machine::off, Machine::on }; Machine fsm; int num; while (1) { cout << "Enter 0/1: "; cin >>liczba; (fsm. *ptrs)(); ) )

15.02.2016
21:30

Wzorzec State jest przeznaczony do projektowania klas, które mają wiele niezależnych stanów logicznych. Przejdźmy od razu do przykładu.

Załóżmy, że opracowujemy klasę sterowania kamerą internetową. Kamera może znajdować się w trzech stanach:

  1. Nie zainicjowano. Nazwijmy to NotConnectedState;
  2. Zainicjalizowany i gotowy do pracy, ale nie są jeszcze przechwytywane żadne klatki. Niech to będzie ReadyState;
  3. Aktywny tryb przechwytywania klatek. Oznaczmy ActiveState .

Ponieważ pracujemy ze wzorcem Stanu, najlepiej zacząć od obrazu Diagramu Stanu:

Zamieńmy teraz ten diagram na kod. Aby nie komplikować implementacji, pomijamy kod do pracy z kamerami internetowymi. W razie potrzeby możesz samodzielnie dodać odpowiednie wywołania funkcji bibliotecznych.

Natychmiast udostępnię pełną listę z minimalnymi komentarzami. Następnie omówimy bardziej szczegółowo kluczowe szczegóły tej implementacji.

#włączać #define DECLARE_GET_INSTANCE(NazwaKlasy) \ static ClassName* getInstance() (\ static ClassName instancja;\ return \ ) class WebCamera ( public: typedef std::string Frame; public: // *********** ***************************************** // Wyjątki // ****** ********************************************** Klasa nieobsługiwana: publiczne std: :wyjątek ( ); public: // ***************************************** ******* *********** // Stany // ****************************** ******* **************** klasa NotConnectedState; klasa ReadyState; klasa ActiveState; klasa State ( public: virtual ~State() ( ) virtual void connect(WebCamera*) ( rzut NotSupported(); ) wirtualne rozłączenie (kamera internetowa*) ( std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->zmieńState(NotConnectedState::getInstance()); ) wirtualny start pustki (WebCamera*) ( rzut NotSupported(); ) wirtualny przystanek pustki (WebCamera*) ( rzut NotSupported(); ) wirtualna ramka getFrame(WebCamera*) ( rzut NotSupported(); ) chroniony: State() ( ) ); // ************************************************** ** klasa NotConnectedState: stan publiczny ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(kamera internetowa*) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->zmieńState(ReadyState::getInstance()); ) void rozłącz(WebCamera*) ( rzut NotSupported(); ) prywatny: NotConnectedState() ( ) ); // ************************************************** ** klasa ReadyState: stan publiczny (public: DECLARE_GET_INSTANCE(ReadyState) void start(kamera internetowa*) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->zmieńState(ActiveState::getInstance()); ) prywatny: ReadyState() ( ) ); // ************************************************** ** klasa ActiveState: stan publiczny (public: DECLARE_GET_INSTANCE(ActiveState) void stop(kamera internetowa*) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam-> << "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } }; public: explicit WebCamera(int camID) : m_camID(camID), m_state(NotConnectedState::getInstance()) { } ~WebCamera() { try { disconnect(); } catch(const NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } } void connect() { m_state->połącz(to); ) void rozłącz() ( m_state->disconnect(this); ) void start() ( m_state->start(this); ) void stop() ( m_state->stop(to); ) Frame getFrame() (zwróć m_state ->getFrame(this); ) private: voidchangeState(State* newState) ( m_state = newState; ) private: int m_camID; Stan* m_stan; );

Zwracam uwagę na makro DECLARE_GET_INSTANCE. Oczywiście odradza się używanie makr w C++. Dotyczy to jednak przypadków, gdy makro pełni funkcję analogową do funkcji szablonowej. W takim przypadku zawsze preferuj to drugie.

W naszym przypadku makro ma na celu zdefiniowanie funkcji statycznej potrzebnej do zaimplementowania . Dlatego jego stosowanie można uznać za uzasadnione. Przecież pozwala ograniczyć duplikację kodu i nie stwarza żadnych poważnych zagrożeń.

Klasy State deklarujemy w klasie głównej - WebCamera. Dla zwięzłości użyłem wbudowanych definicji funkcji składowych wszystkich klas. Jednak w rzeczywistych zastosowaniach lepiej kierować się zaleceniami dotyczącymi rozdzielenia deklaracji i implementacji na pliki h i cpp.

Klasy stanu są deklarowane wewnątrz kamery internetowej, dzięki czemu mają dostęp do prywatnych pól tej klasy. Oczywiście tworzy to niezwykle ścisłe połączenie pomiędzy wszystkimi tymi klasami. Państwa okazują się jednak na tyle specyficzne, że ich ponowne użycie w innych kontekstach nie wchodzi w rachubę.

Podstawą hierarchii klas stanów jest klasa abstrakcyjna WebCamera::State:

Stan klasy ( public: virtual ~State() ( ) wirtualna pustka połącz (kamera internetowa*) ( rzut NotSupported(); ) wirtualna pustka rozłącz (kamera internetowa*) ( std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->zmieńState(NotConnectedState::getInstance()); ) wirtualny start pustki (WebCamera*) ( rzut NotSupported(); ) wirtualny przystanek pustki (WebCamera*) ( rzut NotSupported(); ) wirtualna ramka getFrame(WebCamera*) ( rzut NotSupported(); ) chroniony: State() ( ) );

Wszystkie jego funkcje członkowskie odpowiadają funkcjom samej klasy WebCamera. Delegowanie bezpośrednie ma miejsce:

Klasa WebCamera ( // ... void connect() ( m_state->connect(this); ) void rozłącz() ( m_state->disconnect(this); ) void start() ( m_state->start(this); ) void stop() ( m_state->stop(this); ) Frame getFrame() ( return m_state->getFrame(this); ) // ... State* m_state; )

Kluczową cechą jest to, że obiekt State akceptuje wskaźnik do instancji WebCamera, która go wywołuje. Dzięki temu można mieć tylko trzy obiekty State dla dowolnie dużej liczby kamer. Możliwość taką uzyskuje się poprzez zastosowanie wzorca Singletona. Oczywiście w kontekście przykładu nie uzyskasz z tego znaczącego zysku. Jednak znajomość tej techniki jest nadal przydatna.

Sama klasa WebCamera nie robi praktycznie nic. Jest całkowicie zależny od swoich Państw. A te państwa z kolei określają warunki wykonywania operacji i zapewniają niezbędny kontekst.

Większość funkcji członkowskich WebCamera::State generuje własne WebCamera::NotSupported . Jest to całkowicie odpowiednie zachowanie domyślne. Na przykład, jeśli ktoś spróbuje zainicjować kamerę, która została już zainicjowana, w naturalny sposób otrzyma wyjątek.

Jednocześnie zapewniamy domyślną implementację WebCamera::State::disconnect(). To zachowanie jest odpowiednie dla dwóch z trzech stanów. Dzięki temu zapobiegamy dublowaniu kodu.

Aby zmienić stan, użyj funkcji członka prywatnego WebCamera::changeState() :

Nieważna zmianaState(Stan* nowyState) ( m_state = newState; )

Teraz do realizacji konkretnych państw. W przypadku WebCamera::NotConnectedState wystarczy zastąpić operacje connect() i rozłączyć():

Klasa NotConnectedState: stan publiczny ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(kamera internetowa*) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->zmieńState(ReadyState::getInstance()); ) void rozłącz(WebCamera*) ( rzut NotSupported(); ) prywatny: NotConnectedState() ( ) );

Dla każdego stanu możesz utworzyć pojedynczą instancję. Gwarantujemy to nam deklarując prywatnego konstruktora.

Kolejnym ważnym elementem prezentowanej realizacji jest to, że do nowego Państwa przenosimy się tylko w przypadku powodzenia. Na przykład, jeśli podczas inicjalizacji kamery wystąpi awaria, jest za wcześnie na wejście w stan gotowości. Główną ideą jest pełna zgodność pomiędzy rzeczywistym stanem kamery (w naszym przypadku) a obiektem State.

Zatem aparat jest gotowy do pracy. Utwórzmy odpowiednią klasę WebCamera::ReadyState State:

Klasa ReadyState: stan publiczny (public: DECLARE_GET_INSTANCE(ReadyState) void start(kamera internetowa*) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->zmieńState(ActiveState::getInstance()); ) prywatny: ReadyState() ( ) );

Ze stanu gotowości możemy przejść do aktywnego stanu przechwytywania ramki. W tym celu udostępniona jest operacja start(), którą zaimplementowaliśmy.

Wreszcie dotarliśmy do ostatniego logicznego stanu kamery, WebCamera::ActiveState:

Klasa ActiveState: stan publiczny (public: DECLARE_GET_INSTANCE(ActiveState) void stop(kamera internetowa*) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam->zmieńState(ReadyState::getInstance()); ) Ramka getFrame(WebCamera*) ( std::cout<< "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } };

W tym stanie możesz zatrzymać przechwytywanie klatek za pomocą funkcji stop() . W rezultacie zostaniemy przeniesieni z powrotem do WebCamera::ReadyState. Dodatkowo możemy odbierać klatki, które kumulują się w buforze aparatu. Dla uproszczenia przez „ramkę” rozumiemy zwykły ciąg znaków. W rzeczywistości będzie to pewnego rodzaju tablica bajtów.

Teraz możemy zapisać typowy przykład pracy z naszą klasą WebCamera:

Int main() ( WebCamera cam(0); try ( // kamera w stanie NotConnectedState cam.connect(); // kamera w stanie ReadyState cam.start(); // kamera w stanie ActiveState std::cout<< cam.getFrame() << std::endl; cam.stop(); // Можно было сразу вызвать disconnect() // cam в Состоянии ReadyState cam.disconnect(); // cam в Состоянии NotConnectedState } catch(const WebCamera::NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } return 0; }

W rezultacie na konsolę zostanie wyświetlony następujący komunikat:

Zainicjuj kamerę... Rozpocznij strumień wideo... Pobierz bieżącą klatkę... Bieżącą klatkę Zatrzymaj strumień wideo... Deinicjuj kamerę...

Teraz spróbujmy sprowokować błąd. Wywołajmy funkcję connect() dwa razy z rzędu:

Int main() ( WebCamera cam(0); try ( // kamera w stanie NotConnectedState cam.connect(); // kamera w stanie ReadyState // Ale dla tego stanu operacja connect() nie jest dostępna! cam.connect( ); // Zgłasza wyjątek NotSupported ) catch(const WebCamera::NotSupported& e) ( std::cout<< "Произошло исключение!!!" << std::endl; // ... } catch(...) { // Обрабатываем исключение } return 0; }

Oto, co z tego wynika:

Inicjowanie kamery... Wystąpił wyjątek!!! Deinicjujmy kamerę...

Należy pamiętać, że kamera była nadal deinicjowana. Wywołanie funkcji rozłączenia() nastąpiło w destruktorze kamery internetowej. Te. stan wewnętrzny obiektu pozostaje całkowicie poprawny.

wnioski

Korzystając ze wzorca stanu, można w unikalny sposób przekształcić diagram stanu w kod. Na pierwszy rzut oka wdrożenie okazało się szczegółowe. Doszliśmy jednak do jasnego podziału na możliwe konteksty pracy z główną klasą WebCamera. W rezultacie, pisząc o poszczególnych stanach, mogliśmy skoncentrować się na wąskim zadaniu. I to jest najlepszy sposób na pisanie przejrzystego, zrozumiałego i niezawodnego kodu.