Также известен как 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