Также известен как State

Состояние

Суть паттерна

Позволяет объекту менять своё поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.

Проблема

Давайте разберёмся с терминами.

Что такое состояние?

Это значения всех полей объекта.

Например, самолёт находится в условном состоянии полет, если его высота > 0, скорость > 0 и включен_двигатель == true. Тот же самолёт находится в состоянии погрузка, если мы в аэропорту, высота == 0, включен_двигатель == false.

Что значит менять поведение в зависимости от состояния?

Например, у самолёта есть функция выгрузить пассажиров. Если её выполнить в состоянии разгрузка, произойдёт одно действие, а если выполнить её во время полёта, то произойдёт... что-то совершенно другое.

Что такое стейт-машина?

Стейт-машина — это программа, которая построена вокруг нескольких определённых состояний. Работа этой программы происходит путём постоянных переходов из одного состояния в другое.

Например, наш самолёт можно запрограммировать как стейт-машину с такими состояниями:

Терминология паттерна состояние №1

При этом из одного состояния может быть несколько переходов в другие состояния, например:

Терминология паттерна состояние №2

Паттерн Состояние является одним из способов реализации стейт-машины (хотя и не единственным).

Итак, у вас есть объект, который может находиться в некоторых состояниях. Часть его поведения в этих состояниях должна меняться.

Решение

Паттерн предлагает вынести код, зависящих от определённых состояний объекта в отдельные классы-состояния с общим интерфейсом.

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

Так как все классы-состояния реализуют общий интерфейс, вы можете подставлять новый объект-состояние в основной класс при смене его состояния. Этим и будет достигаться смена поведения в зависимости от состояния.

Аналогия из жизни

Смартфон

Ваш смартфон ведёт себя по-разному, в зависимости от текущего состояния:

  • Когда телефон разблокирован, нажатие кнопок телефона приводит к каким-то действиям.
  • Когда телефон заблокирован, нажатие кнопок приводит к экрану разблокировки.
  • Когда телефон разряжен, нажатие кнопок приводит к экрану зарядки.

В этом примере телефон является Контекстом, а кнопки — его методами. Нажатие кнопок приводит к вызову методов текущего внутреннего Состояния.

Структура

Схема структуры классов паттерна Состояние
  1. Контекст хранит ссылку на объект Конкретного Состояния (но работает с ним только через интерфейс Состояния). Этому объекту и будет делегирована вся работа, связанная с состояниями контекста.

  2. Состояние задаёт общий интерфейс для всех Конкретных Состояний. Здесь должны быть определены методы общие для всех состояний. Реализация этих методов в конкретных состояния будет отличаться.

  3. Конкретные Состояния содержат реализацию поведений, связанных с определённым состоянием Контекста.

  4. И контекст, и объекты конкретных состояний могут решать когда и какое следующее состояние будет выбрано. Чтобы переключить состояние, нужно подать другой объект-состояние в контекст.

Псевдокод

// Общий интерфейс всех состояний.
abstract class State is
    field player: Player

    // Контекст передаёт себя в конструктор состояния, чтобы состояние могло
    // обращаться к его данным и методам в будущем, если потребуется.
    constructor State(player) is
        this.player = player

    abstract method onLock(event)
    abstract method onPlay(event)
    abstract method onNext(event)
    abstract method onPrevious(event)


// Конкретные состояния реализуют методы абстрактного состояния по-своему.
class LockedState is
    method onLock(event) is
        if (player.playing)
            player.changeState(new PlayingState(player))
        else
            player.changeState(new ReadyState(player))

    method onPlay(event) is
        Do nothing.

    method onNext(event) is
        Do nothing.

    method onPrevious(event) is
        Do nothing.


// Они также могут переводить контекст в другие состояния.
class ReadyState is
    method onLock(event) is
        player.changeState(new LockedState(player))

    method onPlay(event) is
        player.startPlayback();
        player.changeState(new PlayingState(player))

    method onNext(event) is
        player.nextSong();

    method onPrevious(event) is
        player.previousSong();


class PlayingState is
    method onLock(event) is
        player.changeState(new LockedState(player))

    method onPlay(event) is
        player.stopPlayback();
        player.changeState(new ReadyState(player))

    method onNext(event) is
        if (event.doubleclick)
            player.nextSong();
        else
            player.fastForward(5)

    method onPrevious(event) is
        if (event.doubleclick)
            player.previous();
        else
            player.previousSong(5)


// Проигрыватель играет роль контекста.
class Player is
    field state: State
    field UI, volume, playlist, currentSong

    constructor Player(player) is
        this.state = new ReadyState(this)
        UI = new UserInterface()

        // Контекст заставляет состояние реагировать на пользовательский ввод
        // вместо себя. Реакция может быть разной в зависимости от того, какое
        // состояние сейчас активно.
        UI.lockButton.onClick(state.onLock)
        UI.playButton.onClick(state.onNext)
        UI.nextButton.onClick(state.onNext)
        UI.prevButton.onClick(state.onPrevious)

    // Сервисные методы контекста, вызываемые состояниями.
    method startPlayback() is
        // ...
    method stopPlayback() is
        // ...
    method nextSong() is
        // ...
    method previousSong() is
        // ...
    method fastForward(time) is
        // ...
    method rewind(time) is
        // ...

Применимость

У вас есть класс, поведение которого кардинально меняется в зависимости от внутреннего состояния. Причём типов состояний много и их код часто изменяется.

Паттерн предлагает создать класс для каждого такого состояния, а затем переместить туда все поля и методы, связанные с состоянием. Контекстный класс будет содержать экземпляр класса-состояния и делегировать ему работу. При изменении состояния, в контекст будет подставляться другой объект.

В разных методах класса вы видите много больших, похожих друг на друга, условных операторов, которые проверяют состояние объекта и выполняют какие-то действия. Такие условные операторы могут проверять одни и те же данные в каждом месте, но выполнять разную работу.

Паттерн предлагает поместить каждую ветку такого условного оператора в собственный класс. В этот класс можно засунуть и все поля, связанные с данным состоянием.

Шаги реализации

  1. Определитесь с классом, который будет отыгрывать роль Контекста. Это может быть как существующий класс, в котором уже есть зависимость от состояния, так и новый класс, если код состояний размазан по нескольким классам.

  2. Создайте интерфейс Состояния. Он должен содержать методы, общие для всех состояний, обнаруженных в Контексте.

  3. Для каждого фактического состояния, создайте класс, реализующий интерфейс Состояния. Переместите весь код, связанный с конкретным состоянием в нужный класс. В конце концов, все методы интерфейса Состояние должны быть реализованы.

  4. Поместите в Контекст поле типа Состояние.

  5. Старые методы Контекста, в которых находился зависимый от состояния код, заменить на вызовы соответствующих методов объекта-состояния.

  6. Либо внутри Контекста, либо внутри классов конкретных состояний разместите код, который изменяет текущее состояние контекста.

Преимущества и недостатки

  • Избавляет от множества больших условных операторов (стейт-машины).
  • Концентрирует в одном месте код, связанный с определённым состоянием.
  • Упрощает код контекста.
  • Может неоправданно усложнить код, если состояний мало и они редко меняются.

Отношения с другими паттернами

  • Мост, Стратегия и Состояние (а также слегка и Адаптер) имеют схожие структуры классов — все они построены на принципе «композиции», то есть делегирования работы другим объектам. Тем не менее они отличаются тем, что решают разные проблемы. Помните, что паттерны — это не только рецепт построения кода определённым образом, но и описание проблем, которые привели к данному решению.

  • Состояние можно рассматривать как надстройку над Стратегией. Оба паттерна используют композицию, чтобы менять поведение основного объекта, делегируя работу вложенным объектам-помощникам. Однако в Стратегии эти объекты не знают друг о друге и никак не связаны. В Состоянии сами конкретные состояния могут переключать контекст.

Реализация в различных языках программирования

Java