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

Состояние

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

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

Проблема

Паттерн Состояние невозможно рассматривать в отрыве от концепции машины состояний (также известной как стейт-машина или конечный автомат).

Основная идея в том, что программа может находиться в одном из нескольких состояний, которые всё время сменяют друг друга. Набор этих состояний, а также переходов между ними предопределён и конечен. Находясь в разных состояниях, программа может по-разному реагировать на одни и те же события, которые происходят с ней.

Такой подход может быть применён и к отдельным объектам. Например, объект Документ может быть в состоянии Черновик, Модерация или Опубликован. В каждом из них его метод опубликовать будет работать по-разному:

  • В первом случае, он отправит документ на модераци.
  • Во втором — отправит документ в публикацию, но при условии, что это сделал администратор.
  • А в последнем — и вовсе ничего не будет делать.

Машину состояний чаще всего реализуют с помощью множества условных операторов, if либо switch, которые проверяют текущее состояние объекта и выполняют соостветствующее поведение. Практически наверняка вы уже реализовали хоть одну машину состояний в своей жизни, даже не зная об этом. Как на счёт вот такого кода?

class Document
    string state;
    // ...
    method publish() {
        switch (state) {
            "draft":
                state = "moderation";
                break;
            "moderation":
                if (currentUser.role == 'admin')
                    state = "published"
                break;
            "published":
                // Do nothing.
        }
    }
    // ...

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

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

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

Решение

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

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

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

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

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

Смартфон

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

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

Структура

Схема структуры классов паттерна Состояние
  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