Модел на държавен дизайн в Java

1. Общ преглед

В този урок ще представим един от поведенческите модели на GoF дизайн - държавния модел.

Отначало ще дадем преглед на нейната цел и ще обясним проблема, който се опитва да реши. След това ще разгледаме UML диаграмата на държавата и прилагането на практическия пример.

2. Държавен модел на дизайн

Основната идея на държавния модел е да позволи на обекта да промени поведението си, без да променя класа си. Също така, чрез прилагането му, кодът трябва да остане по-чист без много if / else изявления.

Представете си, че имаме пакет, който се изпраща до пощенска станция, самият пакет може да бъде поръчан, след това доставен до пощенска служба и накрая да бъде получен от клиент. Сега, в зависимост от действителното състояние, искаме да отпечатаме състоянието му на доставка.

Най-простият подход би бил да добавим някои булеви флагове и да приложим прости if / else изрази във всеки от нашите методи в класа. Това няма да го усложни много в прост сценарий. Това обаче може да усложни и замърси нашия код, когато получим повече състояния за обработка, което ще доведе до още повече if / else изявления.

Освен това цялата логика за всяка от държавите ще бъде разпределена във всички методи. Ето тук може да се приеме, че държавният модел се използва. Благодарение на държавния модел на проектиране, ние можем да капсулираме логиката в специални класове, да приложим принципа на единна отговорност и отворения / затворения принцип, да имаме по-чист и по-поддържаем код.

3. UML диаграма

В UML диаграмата виждаме, че класът Context има свързано състояние, което ще се промени по време на изпълнението на програмата.

Нашият контекст ще делегира поведението на изпълнението на държавата. С други думи, всички входящи искания ще бъдат обработвани от конкретното изпълнение на държавата.

Виждаме, че логиката е разделена и добавянето на нови състояния е просто - то се свежда до добавяне на друго изпълнение на държава, ако е необходимо.

4. Изпълнение

Нека да проектираме нашето приложение. Както вече споменахме, пакетът може да бъде поръчан, доставен и получен, следователно ще имаме три състояния и контекстния клас.

Първо, нека дефинираме нашия контекст, който ще бъде клас на пакет :

public class 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(); } }

Както можем да видим, той съдържа справка за управление на състоянието, забележете методите previousState (), nextState () и printStatus () , където делегираме заданието на обекта на състоянието. Състоянията ще бъдат свързани помежду си и всяко състояние ще зададе друго въз основа на тази препратка, предадена и на двата метода.

Клиентът ще взаимодейства с класа Package , но все пак няма да се налага да се занимава със задаване на състоянията, всичко, което клиентът трябва да направи, е да премине към следващото или предишното състояние.

След това ще имаме PackageState, който има три метода със следните подписи:

public interface PackageState { void next(Package pkg); void prev(Package pkg); void printStatus(); }

Този интерфейс ще бъде реализиран от всеки конкретен държавен клас.

Първото конкретно състояние ще бъде OrderedState :

public class OrderedState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new DeliveredState()); } @Override public void prev(Package pkg) { System.out.println("The package is in its root state."); } @Override public void printStatus() { System.out.println("Package ordered, not delivered to the office yet."); } }

Тук посочваме следващото състояние, което ще настъпи след поръчка на пакета. Подреденото състояние е нашето коренно състояние и ние го маркираме изрично. И при двата метода можем да видим как се обработва преходът между състоянията.

Нека да разгледаме класа DeliveredState :

public class DeliveredState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new ReceivedState()); } @Override public void prev(Package pkg) { pkg.setState(new OrderedState()); } @Override public void printStatus() { System.out.println("Package delivered to post office, not received yet."); } }

Отново виждаме връзката между държавите. Пакетът променя състоянието си от поръчано към доставено, променя се и съобщението в printStatus () .

Последният статус е ReceivedState :

public class ReceivedState implements PackageState { @Override public void next(Package pkg) { System.out.println("This package is already received by a client."); } @Override public void prev(Package pkg) { pkg.setState(new DeliveredState()); } }

Това е мястото, където достигаме последното състояние, можем само да се върнем към предишното състояние.

Вече виждаме, че има известна печалба, тъй като едната държава знае за другата. Правим ги здраво свързани.

5. Тестване

Нека да видим как се държи изпълнението. Първо, нека проверим дали преходите за настройка работят както се очаква:

@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)); }

След това бързо проверете дали нашият пакет може да се върне обратно със състоянието си:

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

След това нека проверим промяната на състоянието и да видим как изпълнението на метода printStatus () променя изпълнението му по време на изпълнение:

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(); } }

Това ще ни даде следния изход:

Package ordered, not delivered to the office yet. Package delivered to post office, not received yet. Package was received by client. This package is already received by a client. Package was received by client.

Тъй като сме променяли състоянието на нашия контекст, поведението се променя, но класът остава същият. Както и API, който използваме.

Също така е настъпил преходът между състоянията, нашият клас е променил състоянието си и следователно поведението си.

6. Недостатъци

Недостатъкът на държавния модел е изплащането при осъществяване на преход между държавите. Това прави държавата твърдо кодирана, което като цяло е лоша практика.

But, depending on our needs and requirements, that might or might not be an issue.

7. State vs. Strategy Pattern

Both design patterns are very similar, but their UML diagram is the same, with the idea behind them slightly different.

First, the strategy pattern defines a family of interchangeable algorithms. Generally, they achieve the same goal, but with a different implementation, for example, sorting or rendering algorithms.

In state pattern, the behavior might change completely, based on actual state.

Next, in strategy, the client has to be aware of the possible strategies to use and change them explicitly. Whereas in state pattern, each state is linked to another and create the flow as in Finite State Machine.

8. Conclusion

Моделът на държавния дизайн е страхотен, когато искаме да избегнем примитивни if / else изявления . Вместо това извличаме логиката за отделни класове и оставяме нашия контекстен обект да делегира поведението на методите, реализирани в държавния клас. Освен това можем да използваме преходите между държавите, където едно състояние може да промени състоянието на контекста.

Като цяло този модел на проектиране е чудесен за относително прости приложения, но за по-усъвършенстван подход можем да разгледаме урока на State Machine на Spring.

Както обикновено, пълният код е достъпен в проекта GitHub.