1. Обзор
В этом уроке мы познакомимся с одним из поведенческих шаблонов проектирования GoF — шаблоном состояния.
Сначала мы дадим обзор его назначения и объясним проблему, которую он пытается решить. Затем мы рассмотрим UML-диаграмму State и реализацию практического примера.
2. Шаблон проектирования состояний
Основная идея паттерна State состоит в том, чтобы позволить объекту изменять свое поведение без изменения его класса. Кроме того, благодаря его реализации код должен оставаться чище без множества операторов if/else.
Представьте, что у нас есть посылка, которая отправляется на почту, саму посылку можно заказать, затем доставить на почту и, наконец, получить клиент. Теперь, в зависимости от фактического состояния, мы хотим вывести статус доставки.
Самым простым подходом было бы добавить несколько логических флагов и применить простые операторы if/else в каждом из наших методов в классе. Это не сильно усложнит простой сценарий. Однако это может усложнить и загрязнить наш код, когда мы получим больше состояний для обработки, что приведет к еще большему количеству операторов if/else.
Кроме того, вся логика для каждого из состояний будет разбросана по всем методам. Здесь можно было бы рассмотреть возможность использования шаблона State. Благодаря шаблону проектирования State мы можем инкапсулировать логику в выделенных классах, применять принцип единой ответственности и принцип открытости/закрытости, иметь более чистый и удобный код.
3. UML-диаграмма
На диаграмме UML мы видим, что у класса Context
есть связанное состояние
, которое будет меняться во время выполнения программы.
Наш контекст собирается делегировать поведение реализации состояния. Другими словами, все входящие запросы будут обрабатываться конкретной реализацией состояния.
Мы видим, что логика разделена и добавление новых состояний простое — все сводится к добавлению еще одной реализации State
, если это необходимо.
4. Реализация
Давайте спроектируем наше приложение. Как уже упоминалось, посылку можно заказать, доставить и получить, поэтому у нас будет три состояния и класс контекста.
Во-первых, давайте определим наш контекст, это будет класс Package :
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. Недостатки
Недостатком шаблона состояния является выигрыш при реализации перехода между состояниями. Это делает состояние жестко запрограммированным, что в целом является плохой практикой.
Но, в зависимости от наших потребностей и требований, это может быть или не быть проблемой.
7. Паттерн «состояние против стратегии»
Оба шаблона проектирования очень похожи, но их UML-диаграмма одинакова, а идея, стоящая за ними, немного отличается.
Во-первых, шаблон стратегии определяет семейство взаимозаменяемых алгоритмов . Как правило, они достигают одной и той же цели, но с разной реализацией, например, алгоритмами сортировки или рендеринга.
В шаблоне состояния поведение может полностью измениться в зависимости от фактического состояния.
Далее, в стратегии, клиент должен знать о возможных стратегиях, чтобы использовать и изменять их в явном виде. Принимая во внимание, что в шаблоне состояния каждое состояние связано с другим и создает поток, как в конечном автомате.
8. Заключение
Шаблон проектирования состояния отлично подходит, когда мы хотим избежать примитивных операторов if/else . Вместо этого мы извлекаем логику для разделения классов и позволяем нашему объекту контекста делегировать поведение методам, реализованным в классе состояния. Кроме того, мы можем использовать переходы между состояниями, когда одно состояние может изменить состояние контекста.
В общем, этот шаблон проектирования отлично подходит для относительно простых приложений, но для более продвинутого подхода мы можем взглянуть на учебник Spring’s State Machine .
Как обычно, полный код доступен на проекте GitHub .