«1. Обзор

В этом уроке мы познакомимся с одним из поведенческих шаблонов проектирования GoF — шаблоном State.

Сначала мы дадим обзор его назначения и объясним проблему, которую он пытается решить. Затем мы рассмотрим UML-диаграмму State и реализацию практического примера.

2. Шаблон проектирования состояния

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

Представьте, что у нас есть посылка, которая отправляется на почту, саму посылку можно заказать, затем доставить на почту и, наконец, получить клиент. Теперь, в зависимости от фактического состояния, мы хотим вывести статус доставки.

Самый простой подход — добавить несколько логических флагов и применить простые операторы if/else в каждом из наших методов в классе. Это не сильно усложнит простой сценарий. Однако это может усложнить и загрязнить наш код, когда мы получим больше состояний для обработки, что приведет к еще большему количеству операторов if/else.

Кроме того, вся логика для каждого из состояний была бы разбросана по всем методам. Здесь можно было бы рассмотреть возможность использования шаблона State. Благодаря шаблону проектирования State мы можем инкапсулировать логику в выделенных классах, применять принцип единой ответственности и принцип открытости/закрытости, иметь более чистый и удобный код.

3. Диаграмма UML

UML diagram of state design pattern

На диаграмме UML мы видим, что у класса Context есть связанное состояние, которое будет изменяться во время выполнения программы.

Наш контекст делегирует поведение реализации состояния. Другими словами, все входящие запросы будут обрабатываться конкретной реализацией состояния.

Мы видим, что логика разделена и добавление новых состояний простое — все сводится к добавлению еще одной реализации состояния, если это необходимо.

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 State Machine.