State Design Pattern v Jave

1. Prehľad

V tomto tutoriáli predstavíme jeden z návrhových vzorov správania GoF - vzor State.

Spočiatku poskytneme prehľad jeho účelu a vysvetlíme problém, ktorý sa snaží vyriešiť. Potom sa pozrieme na UML diagram štátu a implementáciu praktického príkladu.

2. Uveďte návrhový vzor

Hlavná myšlienka štátneho vzoru je: umožniť objektu zmeniť svoje správanie bez zmeny jeho triedy. Jeho implementáciou by mal kód zostať čistejší bez mnohých príkazov if / else.

Predstavte si, že máme balík, ktorý je odoslaný na poštu, samotný balík je možné objednať, potom doručiť na poštu a nakoniec prijať klientom. Teraz, v závislosti od skutočného stavu, chceme vytlačiť jeho stav doručenia.

Najjednoduchším prístupom by bolo pridať nejaké logické príznaky a použiť jednoduché príkazy if / else v rámci každej z našich metód v triede. To ho v jednoduchom scenári príliš nekomplikuje. Môže to však skomplikovať a znečistiť náš kód, keď dostaneme viac štátov na spracovanie, čo bude mať za následok ešte viac príkazov if / else.

Okrem toho by sa všetka logika pre každý zo štátov rozšírila do všetkých metód. Toto je miesto, kde sa môže uvažovať o použití štátneho vzoru. Vďaka návrhovému vzoru State môžeme logiku zapuzdriť do vyhradených tried, aplikovať princíp Single Responsibility Principle a Open / Closed Principle, mať čistejší a udržiavateľnejší kód.

3. Diagram UML

Na UML diagrame to vidíme Kontext trieda má pridružené Štát čo sa počas vykonávania programu zmení.

Náš kontext bude smerovať delegovať správanie na implementáciu štátu. Inými slovami, všetky prichádzajúce žiadosti bude vybavovať konkrétna implementácia stavu.

Vidíme, že logika je oddelená a pridávanie nových stavov je jednoduché - prichádza na rad pridávanie ďalších Štát implementácia, ak je to potrebné.

4. Implementácia

Poďme navrhnúť našu aplikáciu. Ako už bolo spomenuté, balík je možné objednať, doručiť a prijať, preto budeme mať tri štáty a kontextovú triedu.

Najskôr si definujme náš kontext, bude to Balíček trieda:

verejná trieda Package {private PackageState state = new OrderedState (); // getter, setter public void previousState () {state.prev (this); } public void nextState () {state.next (this); } public void printStatus () {state.printStatus (); }}

Ako vidíme, obsahuje referenciu na správu štátu, upozornenie previousState (), nextState () a printStatus () metódy, keď úlohu delegujeme na objekt state. Štáty budú navzájom prepojené a každý štát nastaví iný na základe toto odkaz prešlo na obidve metódy.

Klient bude komunikovať s Balíček triedy, napriek tomu sa nebude musieť zaoberať nastavením stavov, klient musí len prejsť do ďalšieho alebo predchádzajúceho stavu.

Ďalej budeme mať PackageState ktorá má tri metódy s nasledujúcimi podpismi:

verejné rozhranie PackageState {void next (Package pkg); void prev (balenie v balení); void printStatus (); }

Toto rozhranie bude implementované každou konkrétnou stavovou triedou.

Prvý konkrétny stav bude OrderedState:

verejná trieda OrderedState implementuje PackageState {@Override public void next (balíček pkg) {pkg.setState (nový DeliveredState ()); } @Override public void prev (balík balíkov) {System.out.println ("Balík je v koreňovom stave."); } @Override public void printStatus () {System.out.println ("Balík je objednaný, ešte nebol doručený do kancelárie."); }}

Tu ukážeme na ďalší stav, ktorý nastane po objednaní balíka. Zoradený stav je náš koreňový stav a my ho výslovne označíme. Na oboch metódach môžeme vidieť, ako je riešený prechod medzi stavmi.

Poďme sa pozrieť na Doručený štát trieda:

verejná trieda DeliveredState implementuje PackageState {@Override public void next (balíček pkg) {pkg.setState (nový ReceivedState ()); } @Override public void prev (balík v balení) {pkg.setState (nový OrderedState ()); } @Override public void printStatus () {System.out.println ("Balík doručený na poštu, zatiaľ neprijatý."); }}

Opäť vidíme prepojenie medzi štátmi. Balík mení svoj stav z objednaného na doručený, správa v printStatus () zmeny tiež.

Posledný stav je ReceivedState:

verejná trieda ReceivedState implementuje PackageState {@Override public void next (balík pkg) {System.out.println ("Tento balík už klient dostal."); } @Override public void prev (balík v balení) {pkg.setState (nový DeliveredState ()); }}

Tu sa dostávame do posledného stavu, môžeme sa vrátiť iba do predchádzajúceho stavu.

Už vidíme, že existuje istý výnos, pretože jeden štát vie o druhom. Robíme ich pevne prepojenými.

5. Testovanie

Pozrime sa, ako sa správa implementácia. Najprv skontrolujeme, či prechody nastavenia fungujú podľa očakávania:

@Test public void givenNewPackage_whenPackageReceived_thenStateReceived () {Package pkg = new Package (); assertThat (pkg.getState (), instanceOf (OrderedState.class)); pkg.nextState (); assertThat (pkg.getState (), instanceOf (DeliveredState.class)); pkg.nextState (); assertThat (pkg.getState (), instanceOf (ReceivedState.class)); }

Potom rýchlo skontrolujte, či sa náš balík môže vrátiť späť so svojim stavom:

@Test public void givenDeliveredPackage_whenPrevState_thenStateOrdered () {Package pkg = new Package (); pkg.setState (new DeliveredState ()); pkg.previousState (); assertThat (pkg.getState (), instanceOf (OrderedState.class)); }

Po tom, overme zmenu stavu a uvidíme, ako bude implementácia printStatus () metóda mení svoju implementáciu za behu:

public class StateDemo {public static void main (String [] args) {Package pkg = new Package (); pkg.printStatus (); pkg.nextState (); pkg.printStatus (); pkg.nextState (); pkg.printStatus (); pkg.nextState (); pkg.printStatus (); }}

Získate tým nasledujúci výstup:

Balík objednaný, do kancelárie zatiaľ nedoručený. Balík doručený na poštu, zatiaľ neprijatý. Balík bol prijatý klientom. Tento balík už klient dostal. Balík bol prijatý klientom.

Keď sme zmenili stav nášho kontextu, zmenilo sa aj správanie, ale trieda zostala rovnaká. Rovnako ako API, ktoré používame.

Taktiež došlo k prechodu medzi stavmi, naša trieda zmenila svoj stav a následne aj správanie.

6. Nevýhody

Nevýhodou stavu je návratnosť pri implementácii prechodu medzi štátmi. Vďaka tomu je štát pevne zakódovaný, čo je všeobecne zlá prax.

Ale v závislosti od našich potrieb a požiadaviek to môže alebo nemusí byť problém.

7. Stav vs. Stratégia

Oba návrhové vzory sú si veľmi podobné, ale ich diagram UML je rovnaký, s myšlienkou za nimi mierne odlišnou.

Po prvé, strategický vzor definuje rodinu zameniteľných algoritmov. Spravidla dosahujú rovnaký cieľ, ale s inou implementáciou, napríklad algoritmami triedenia alebo vykresľovania.

Podľa stavu sa správanie môže úplne zmeniť, na základe skutočného stavu.

Ďalšie, v stratégii si klient musí byť vedomý možných stratégií, ktoré môže použiť a výslovne ich zmeniť. Zatiaľ čo v stavovom vzore je každý stav prepojený s iným a vytvára tok ako v stroji konečných stavov.

8. Záver

Stavový vzor je vynikajúci, keď chceme vyhnúť sa primitívnym výrokom if / else. Namiesto toho sme extrahovať logiku do samostatných tried a nechajme naše kontextový objekt delegovať správanie k metódam implementovaným v štátnej triede. Okrem toho môžeme využiť prechody medzi stavmi, kde jeden štát môže zmeniť stav kontextu.

Všeobecne je tento návrhový vzor vhodný pre relatívne jednoduché aplikácie, ale pre pokročilejší prístup si môžeme pozrieť príručku Spring's State Machine.

Kompletný kód je ako obvykle k dispozícii v projekte GitHub.


$config[zx-auto] not found$config[zx-overlay] not found