Простые стейт-машины на службе у разработчика
Представьте на минутку обычного программиста. Допустим, его зовут Вася и ему нужно сделать анимированную менюшку на сайт/десктоп приложение/мобильный апп. Знаете, которые выезжают сверху вниз, как меню у окна Windows или меню с яблочком у OS X. Вот такое.
Начинает он с одного выпадающего окошка, тестирует анимацию, выставляет ease out 100% и наслаждается полученным результатом. Но вскоре он понимает, что для того, чтобы управлять менюшкой, хорошо бы знать закрыто оно сейчас или нет. Мы-то с вами тут программисты опытные, все понимаем, что нужно добавить флаг. Не вопрос, флаг есть.
var opened = false;
Вроде, работает. Но, если быстро кликать по кнопке, меню начинает моргать, открываясь и закрываясь не успев доанимироваться в конечное состояние. Вася добавляет флаг animating. Теперь код у нас такой:
var opened = false; var animating = false; function onClick(event)
Через какое-то время Васе говорят, что меню может быть полностью выключено и неактивно. Не вопрос! Мы-то с вами тут программисты опытные, все понимаем, что… нужно добавить ЕЩЕ ОДИН ФЛАГ! И, всего-то через пару дней разработки, код меню уже пестрит двустрочными IF-ами типа вот такого:
if (enabled && opened && !animating && !selected && finishedTransition && !endOfTheWorld && . )
Вася начинает задаваться вопросами: как вообще может быть, что animating == true и enabled == false; почему у него время от времени все глючит; как тут вообще поймешь в каком состоянии находится меню. Ага! Состояния. О них дальше и пойдет речь.
Знакомьтесь, это Вася.
Состояние
Вася уже начинает понимать, что многие комбинации флагов не имеют смысла, а остальные можно легко описать парой слов, например: Disabled, Idle, Animating, Opened. Все мы тут программисты опытные, сразу вспоминаем про state machines. Но, для Васи придется рассказать что это и зачем. Простым языком, без всяких математических терминов.
У нас есть объект, например, вышеупомянутая менюшка. Объект всегда находится в каком-то одном состоянии и реагируя на различные события может между этими состояниями переходить. Обычно состояния, события и переходы удобно описывать вот такими схемами (кружочками обозначены начальное и конечные состояния):
Из схемы понятно, что из состояния Inactive в Active можно попасть только по событию Begin, а из состояния Paused можно попасть как и в Active, так и в Inactive. Такую простую концепцию почему-то называют «Конечный Автомат» или «Finite State Machine», что очень пугает обычных людей.
По завету ООП, состояния должны быть скрыты внутри объекта и просто так снаружи не доступны. Например, у объекта во время работы может быть 20 разных состояний, но внешнее API на вопрос «чо как дела?» отвечает «ничо так» на 19 из них и только на 1 ругается матом, что проср*ли все полимеры.
Следуя концепции стейт машин, очень легко структурировать код так, что всегда будет ясно что и как делает тот или иной объект. Всегда будет понятно, что что-то пошло не так, если система вдруг попыталась перейти в недоступное из данного состояния состояние. А события, которые вдруг посмели прийти в неправильное время, можно смело игнорировать и не бояться, что что-нибудь сломается.

Самая простая в мире стейт машина
Допустим, теперь Вася делает проект на C# и ему нужна простая стейт машина для одного типа объектов. Он пишет что-то типа такого:
private enum State < Disabled, Idle, Animating >private State state; void setState(State value) < state = value; switch (state) < case State.Disabled: . case State.Idle: . case State.Animating : . break; >>
А вот так обрабатывает события в зависимости от текущего состояния:
void event1Handler() < switch (state) < case State.Idle: . break; >>
Но, мы-то с вами тут программисты опытные, все понимаем, что метод setState в итоге разрастется на пару десятков страниц, что (как написано в учебниках) не есть хорошо.
State Pattern
Погуглив пару часов, Вася решает, что State Pattern идеально подходит в данной ситуации. Тем более, что старшие программисты все время соревнуются кто больше паттернов запихнет в свой апп, так что, решает Вася, паттерны это дело важное.
Например, для State Pattern можно сделать интерфейс IState:
public interface IState
И по отдельному классу для каждого состояния, которые этот интерфейс имплементят. В теории выглядит красиво и 100% по учебнику.
Но, во-первых, для каждой несчастной мелкой стейт машины нужно городить уйму классов, что само по себе небыстро. Во-вторых, рано или поздно начнутся проблемы с доступом к общим данным. Где их хранить? В основном классе? А как классы-состояния получат к ним доступ? А как мне тут за 15 минут перед дедлайном впилить быстро мелкий хак в обход правил? И подобные проблемы взаимодействия, которые будут сильно тормозить разработку.
Реализация на основе особенностей языка
Некоторые языки программирования облегчают решение тех или иных задач. В Ruby, например, так вообще есть целый DSL (и не один) для создания конечных автоматов. А в C# конечный автомат можно упростить через Reflection. Вот как-то так:
- наследуемся от класса FiniteStateMachine,
- создаем методы с названием stateName_eventName(), которые автоматически вызываются при переходе по состояниям и при обработке событий
Реализовав систему описанную выше, Вася понимает, что у нее тоже больше минусов, чем плюсов:
- Нужно наследоваться от класса FiniteStateMachine,
- В реакциях на кастомные события также нужно писать большие switch конструкции,
- Нет возможности передать параметры при изменении состояния.
Фреймворк
А тем временем, Вася уже вовсю стал вникать в теорию стейт машин и решил, что хорошо бы иметь возможность формально их описывать через API или (о Боже) через XML, что в теории звучит круто. Мы-то с вами тут программисты опытные, все понимаем, что нужно писать свой фреймворк. Потому что другие не подходят, так как у всех у них есть один фатальный недостаток.
Вася решил, что с помощью его фреймворка можно будет быстро и легко создать стейт машину без необходимости писать много ненужного кода. Фреймворк не будет накладывать никаких ограничений на разработчика. Все вокруг будут веселы и жизнерадостны.
Я попробовал множество фреймворков на разных языках, несколько подобных написал сам. И всегда для описания конечного автомата средствами фреймворка требовалось больше кода, чем в простом примере. Все они накладывают те или иные ограничения, а многие пытаются делать сразу столько всего, что для того, чтобы разобраться, как же тут все-таки создать несложную стейт машину, приходится продолжительное время рыться в документации.
Вот, например, описание конечного автомата фреймворком stateless:
var phoneCall = new StateMachine(State.OffHook); phoneCall.Configure(State.OffHook) .Permit(Trigger.CallDialed, State.Ringing); phoneCall.Configure(State.Ringing) .Permit(Trigger.HungUp, State.OffHook) .Permit(Trigger.CallConnected, State.Connected); phoneCall.Configure(State.Connected) .OnEntry(() => StartCallTimer()) .OnExit(() => StopCallTimer()) .Permit(Trigger.LeftMessage, State.OffHook) .Permit(Trigger.HungUp, State.OffHook) .Permit(Trigger.PlacedOnHold, State.OnHold);
Но, пробившись через создание стейт машины, можно воспользоваться полезными функциями, которые предоставляет фреймворк. В основном это: проверка правильности переходов, синхронизация зависимых стейт машин и суб-стейт машин и всяческая защита от дурака.
XML
XML — это отдельное зло. Кто-то когда-то придумал использовать его для написания конфигов. Стадо леммингов java разработчиков длительное время молилось на него. А теперь никто уже и не знает зачем все используют XML, но продолжают бить всех, кто пытается от него избавиться.
Вася тоже загорелся идеей, что можно все сконфигурировать в XML и НЕ ПИСАТЬ НИ СТРОЧКИ КОДА! В итоге в его фреймворке отдельно лежат XML файлы примерно такого содержания:
Класс! И никакого программирования. Но, мы-то с вами тут программисты опытные, все понимаем, что программирование никуда не ушло. Вася заменил кусок императивного кода на кусок декларативного кода, добавив при этом во фреймворк интерпретатор XML, который все еще в пару раз усложнил. А потом попробуй это отдебажить, когда код на разных языках и разбросан по проекту.
Соглашение
И тут Васе все это надоело и он вернулся обратно к самому простому в мире конечному автомату. Он его немного переделал и придумал правила как писать в нем код.
UPDATE: спасибо за комментарии. Здесь действительно не хватало небольшого объяснения.
У нас есть несколько состояний. Переход между ними — это транзакция из атомарных операций, то есть они все происходят всегда вместе, в правильном порядке и между ними не может вклиниться еще какой-то код. При смене состояния с A на B происходит следующее: выполняется код выхода из состояния A, состояние меняется с A на B, выполняется код входа в состояние B.
Для перехода на состояние A нужно вызвать метод stateA, который выполнит нужную логику и вызовет setState(A). Самому вызывать setState(A) крайне не рекомендуется.
/** * Названия состояний описываются enum или строковыми константами, если язык не поддерживает enums. */ private enum State < Disabled, Idle, Animating >/** * Текущее состояние всегда скрыто. Иногда, бывает полезно добавить еще и переменную с предыдущим состоянием. */ private State state; /** * Все смены состояний происходят только через вызов методов state(). * В них сперва может быть выполнена логика для выхода из КОНКРЕТНОГО предыдущего состояния в КОНКРЕТНОЕ новое. * После чего выполняется setState(newValue) и специфическая для состояния логика. */ void stateDisabled() < switch (state) < case State.Idle: break; >setState(State.Disabled); // State Disabled enter logic > /** * У функций смены состояний могут быть параметры. * stateIdle(0); */ void stateIdle(int data) < setState(State.Idle); // State Idle enter logic >void stateAnimating() < setState(State.Animating); // State Animating enter logic >/** * Обычно setState состоит только из * state = value; * или еще prevState = state; если нужно хранить предыдущее состояние. * Но, также здесь находится общая логика выхода из предыдущего состояния. */ void setState(State value) < switch ( state ) < case State.Animating: // state Animating exit logic break; // other states >state = value; > /** * Обработчики событий делают только то, что можно в текущем состоянии. */ void event1Handler() < switch (state) < case State.Idle: // state Idle logic break; // other states >>
UPDATE: В setState() пишется уникальная логика выхода из состояния, а в stateB() возможна специфическая логика выхода из состояния A при переходе в B. Но очень редко используется.
Простое соглашение для написания стейт машин. Оно достаточно гибкое и имеет следующие плюсы:
- почти вся логика при смене состояний находится в методах stateA(), что позволяет разбить гигантский switch в setState() и сделать код более читаемым,
- смена состояния происходит только через методы stateA(), что облегчает отладку,
- новому состоянию легко можно передавать параметры, например, если у книги есть состояние Page, то перейти на новую страницу можно просто сменив состояния вызвав statePage(42)
- в обработчиках событий всегда понятно какая логика выполняется в каких состояниях,
- все члены команды знают где писать логику для входа и выхода из состояния,
- нет необходимости в каком-то фреймворке и предварительной конфигурации конечного автомата,
- есть возможность грязно все похачить в последний момент, если уж совсем подругому никак.
Как и во всех соглашениях, какой-то код может сперва находиться в одном месте, но потом у него появится другой смысл, или окажется, что он где-то дублируется. Тогда мы можем его перенести в другое место. Никто нам не запрещает. Все-таки код не вытесан из камня, это всего лишь текст, который (о ужас!) можно и нужно менять с развитием проекта.
UPDATE: а setState() вполне можно заменить одним сеттером для наглядности.
Заключение
На этом заканчивается увлекательное приключение Васи в мире стейт машин. А ведь впереди еще столько всего интересного. Отдельного топика бы только заслужили параллельные и зависимые стейт машины.
Я надеюсь, что, если вы еще не используете стейт машины повсеместно, эта статья перетянет вас на сторону добра; если вы пишите свой уберфреймворк для работы со стейт машинами, она поможет свежим взглядом посмотреть на то, что у вас получается.
Я надеюсь, что эта статья поможет разработчикам задуматься где и когда стоит использовать паттерны и фреймворки, и что описанное соглашение по оформлению стейт машин окажется кому-то полезным.
Состояние
Состояние — это поведенческий паттерн проектирования, который позволяет объектам менять поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.

Проблема
Паттерн Состояние невозможно рассматривать в отрыве от концепции машины состояний, также известной как стейт-машина или конечный автомат Конечный автомат: https://refactoring.guru/ru/fsm .

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

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

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

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

Объект проигрывателя содержит объект-состояние, которому и делегирует основную работу. Изменяя состояния, можно менять то, как ведут себя элементы управления проигрывателя.
// Общий интерфейс всех состояний. abstract class State is protected field player: AudioPlayer // Контекст передаёт себя в конструктор состояния, чтобы // состояние могло обращаться к его данным и методам в // будущем, если потребуется. constructor State(player) is this.player = player abstract method clickLock() abstract method clickPlay() abstract method clickNext() abstract method clickPrevious() // Конкретные состояния реализуют методы абстрактного состояния // по-своему. class LockedState extends State is // При разблокировке проигрователя с заблокированными // клавишами он может принять одно из двух состояний. method clickLock() is if (player.playing) player.changeState(new PlayingState(player)) else player.changeState(new ReadyState(player)) method clickPlay() is // Ничего не делать. method clickNext() is // Ничего не делать. method clickPrevious() is // Ничего не делать. // Конкретные состояния сами могут переводить контекст в другое // состояние. class ReadyState extends State is method clickLock() is player.changeState(new LockedState(player)) method clickPlay() is player.startPlayback() player.changeState(new PlayingState(player)) method clickNext() is player.nextSong() method clickPrevious() is player.previousSong() class PlayingState extends State is method clickLock() is player.changeState(new LockedState(player)) method clickPlay() is player.stopPlayback() player.changeState(new ReadyState(player)) method clickNext() is if (event.doubleclick) player.nextSong() else player.fastForward(5) method clickPrevious() is if (event.doubleclick) player.previous() else player.rewind(5) // Проигрыватель выступает в роли контекста. class AudioPlayer is field state: State field UI, volume, playlist, currentSong constructor AudioPlayer() is this.state = new ReadyState(this) // Контекст заставляет состояние реагировать на // пользовательский ввод вместо себя. Реакция может быть // разной, в зависимости от того, какое состояние сейчас // активно. UI = new UserInterface() UI.lockButton.onClick(this.clickLock) UI.playButton.onClick(this.clickPlay) UI.nextButton.onClick(this.clickNext) UI.prevButton.onClick(this.clickPrevious) // Другие объекты тоже должны иметь возможность заменять // состояние проигрывателя. method changeState(state: State) is this.state = state // Методы UI будут делегировать работу активному состоянию. method clickLock() is state.clickLock() method clickPlay() is state.clickPlay() method clickNext() is state.clickNext() method clickPrevious() is state.clickPrevious() // Сервисные методы контекста, вызываемые состояниями. method startPlayback() is // . method stopPlayback() is // . method nextSong() is // . method previousSong() is // . method fastForward(time) is // . method rewind(time) is // .
Применимость
Когда у вас есть объект, поведение которого кардинально меняется в зависимости от внутреннего состояния, причём типов состояний много, и их код часто меняется.
Паттерн предлагает выделить в собственные классы все поля и методы, связанные с определёнными состояниями. Первоначальный объект будет постоянно ссылаться на один из объектов-состояний, делегируя ему часть своей работы. Для изменения состояния в контекст достаточно будет подставить другой объект-состояние.
Когда код класса содержит множество больших, похожих друг на друга, условных операторов, которые выбирают поведения в зависимости от текущих значений полей класса.
Паттерн предлагает переместить каждую ветку такого условного оператора в собственный класс. Тут же можно поселить и все поля, связанные с данным состоянием.
Когда вы сознательно используете табличную машину состояний, построенную на условных операторах, но вынуждены мириться с дублированием кода для похожих состояний и переходов.
Паттерн Состояние позволяет реализовать иерархическую машину состояний, базирующуюся на наследовании. Вы можете отнаследовать похожие состояния от одного родительского класса и вынести туда весь дублирующий код.
Шаги реализации
- Определитесь с классом, который будет играть роль контекста. Это может быть как существующий класс, в котором уже есть зависимость от состояния, так и новый класс, если код состояний размазан по нескольким классам.
- Создайте общий интерфейс состояний. Он должен описывать методы, общие для всех состояний, обнаруженных в контексте. Заметьте, что не всё поведение контекста нужно переносить в состояние, а только то, которое зависит от состояний.
- Для каждого фактического состояния создайте класс, реализующий интерфейс состояния. Переместите код, связанный с конкретными состояниями в нужные классы. В конце концов, все методы интерфейса состояния должны быть реализованы во всех классах состояний. При переносе поведения из контекста вы можете столкнуться с тем, что это поведение зависит от приватных полей или методов контекста, к которым нет доступа из объекта состояния. Существует парочка способов обойти эту проблему. Самый простой — оставить поведение внутри контекста, вызывая его из объекта состояния. С другой стороны, вы можете сделать классы состояний вложенными в класс контекста, и тогда они получат доступ ко всем приватным частям контекста. Но последний способ доступен только в некоторых языках программирования (например, Java, C#).
- Создайте в контексте поле для хранения объектов-состояний, а также публичный метод для изменения значения этого поля.
- Старые методы контекста, в которых находился зависимый от состояния код, замените на вызовы соответствующих методов объекта-состояния.
- В зависимости от бизнес-логики, разместите код, который переключает состояние контекста либо внутри контекста, либо внутри классов конкретных состояний.
Преимущества и недостатки
- Избавляет от множества больших условных операторов машины состояний.
- Концентрирует в одном месте код, связанный с определённым состоянием.
- Упрощает код контекста.
- Может неоправданно усложнить код, если состояний мало и они редко меняются.
Отношения с другими паттернами
- Мост, Стратегия и Состояние (а также слегка и Адаптер) имеют схожие структуры классов — все они построены на принципе «композиции», то есть делегирования работы другим объектам. Тем не менее, они отличаются тем, что решают разные проблемы. Помните, что паттерны — это не только рецепт построения кода определённым образом, но и описание проблем, которые привели к данному решению.
- Состояние можно рассматривать как надстройку над Стратегией. Оба паттерна используют композицию, чтобы менять поведение основного объекта, делегируя работу вложенным объектам-помощникам. Однако в Стратегии эти объекты не знают друг о друге и никак не связаны. В Состоянии сами конкретные состояния могут переключать контекст.
Примеры реализации паттерна

Не втыкай в транспорте
Лучше почитай нашу книгу о паттернах проектирования.
Теперь это удобно делать даже во время поездок в общественном транспорте.

Эта статья является частью нашей электронной книги Погружение в Паттерны Проектирования.

- Премиум контент
- Книга о паттернах
- Курс по рефакторингу
- Введение в рефакторинг
- Чистый код
- Технический долг
- Когда рефакторить
- Как рефакторить
- Раздувальщики
- Длинный метод
- Большой класс
- Одержимость элементарными типами
- Длинный список параметров
- Группы данных
- Операторы switch
- Временное поле
- Отказ от наследства
- Альтернативные классы с разными интерфейсами
- Расходящиеся модификации
- Стрельба дробью
- Параллельные иерархии наследования
- Комментарии
- Дублирование кода
- Ленивый класс
- Класс данных
- Мёртвый код
- Теоретическая общность
- Завистливые функции
- Неуместная близость
- Цепочка вызовов
- Посредник
- Неполнота библиотечного класса
- Составление методов
- Извлечение метода
- Встраивание метода
- Извлечение переменной
- Встраивание переменной
- Замена переменной вызовом метода
- Расщепление переменной
- Удаление присваиваний параметрам
- Замена метода объектом методов
- Замена алгоритма
- Перемещение метода
- Перемещение поля
- Извлечение класса
- Встраивание класса
- Сокрытие делегирования
- Удаление посредника
- Введение внешнего метода
- Введение локального расширения
- Самоинкапсуляция поля
- Замена простого поля объектом
- Замена значения ссылкой
- Замена ссылки значением
- Замена поля-массива объектом
- Дублирование видимых данных
- Замена однонаправленной связи двунаправленной
- Замена двунаправленной связи однонаправленной
- Замена магического числа символьной константой
- Инкапсуляция поля
- Инкапсуляция коллекции
- Замена кодирования типа классом
- Замена кодирования типа подклассами
- Замена кодирования типа состоянием/стратегией
- Замена подкласса полями
- Разбиение условного оператора
- Объединение условных операторов
- Объединение дублирующихся фрагментов в условных операторах
- Удаление управляющего флага
- Замена вложенных условных операторов граничным оператором
- Замена условного оператора полиморфизмом
- Введение Null-объекта
- Введение проверки утверждения
- Переименование метода
- Добавление параметра
- Удаление параметра
- Разделение запроса и модификатора
- Параметризация метода
- Замена параметра набором специализированных методов
- Передача всего объекта
- Замена параметра вызовом метода
- Замена параметров объектом
- Удаление сеттера
- Сокрытие метода
- Замена конструктора фабричным методом
- Замена кода ошибки исключением
- Замена исключения проверкой условия
- Подъём поля
- Подъём метода
- Подъём тела конструктора
- Спуск метода
- Спуск поля
- Извлечение подкласса
- Извлечение суперкласса
- Извлечение интерфейса
- Свёртывание иерархии
- Создание шаблонного метода
- Замена наследования делегированием
- Замена делегирования наследованием
- Введение в паттерны
- Что такое Паттерн?
- История паттернов
- Зачем знать паттерны?
- Критика паттернов
- Классификация паттернов
- Фабричный метод
- Абстрактная фабрика
- Строитель
- Прототип
- Одиночка
- Адаптер
- Мост
- Компоновщик
- Декоратор
- Фасад
- Легковес
- Заместитель
- Цепочка обязанностей
- Команда
- Итератор
- Посредник
- Снимок
- Наблюдатель
- Состояние
- Стратегия
- Шаблонный метод
- Посетитель
- C#
- C++
- Go
- Java
- PHP
- Python
- Ruby
- Rust
- Swift
- TypeScript
Что такое стейт в программировании
Состояние (State) — шаблон проектирования, который позволяет объекту изменять свое поведение в зависимости от внутреннего состояния.
Когда применяется данный паттерн?
- Когда поведение объекта должно зависеть от его состояния и может изменяться динамически во время выполнения
- Когда в коде методов объекта используются многочисленные условные конструкции, выбор которых зависит от текущего состояния объекта
UML-диаграмма данного шаблона проектирования предлагает следующую систему:

Формальное определение паттерна на C#:
class Program < static void Main() < Context context = new Context(new StateA()); context.Request(); // Переход в состояние StateB context.Request(); // Переход в состояние StateA >> abstract class State < public abstract void Handle(Context context); >class StateA : State < public override void Handle(Context context) < context.State = new StateB(); >> class StateB : State < public override void Handle(Context context) < context.State = new StateA(); >> class Context < public State State < get; set; >public Context(State state) < this.State = state; >public void Request() < this.State.Handle(this); >>
Участники паттерна
- State : определяет интерфейс состояния
- Классы StateA и StateB — конкретные реализации состояний
- Context : представляет объект, поведение которого должно динамически изменяться в соответствии с состоянием. Выполнение же конкретных действий делегируется объекту состояния
Например, вода может находиться в ряде состояний: твердое, жидкое, парообразное. Допустим, нам надо определить класс Вода, у которого бы имелись методы для нагревания и заморозки воды. Без использования паттерна Состояние мы могли бы написать следующую программу:
class Program < static void Main(string[] args) < Water water = new Water(WaterState.LIQUID); water.Heat(); water.Frost(); water.Frost(); Console.Read(); >> enum WaterState < SOLID, LIQUID, GAS >class Water < public WaterState State < get; set; >public Water(WaterState ws) < State = ws; >public void Heat() < if(State==WaterState.SOLID) < Console.WriteLine("Превращаем лед в жидкость"); State = WaterState.LIQUID; >else if (State == WaterState.LIQUID) < Console.WriteLine("Превращаем жидкость в пар"); State = WaterState.GAS; >else if (State == WaterState.GAS) < Console.WriteLine("Повышаем температуру водяного пара"); >> public void Frost() < if (State == WaterState.LIQUID) < Console.WriteLine("Превращаем жидкость в лед"); State = WaterState.SOLID; >else if (State == WaterState.GAS) < Console.WriteLine("Превращаем водяной пар в жидкость"); State = WaterState.LIQUID; >> >Вода имеет три состояния, и в каждом методе нам надо смотреть на текущее состояние, чтобы произвести действия. В итоге с трех состояний уже получается нагромождение условных конструкций. Да и самим методов в классе Вода может также быть множество, где также надо будет действовать в зависимости от состояния. Поэтому, чтобы сделать программу более гибкой, в данном случае мы можем применить паттерн Состояние:
class Program < static void Main(string[] args) < Water water = new Water(new LiquidWaterState()); water.Heat(); water.Frost(); water.Frost(); Console.Read(); >> class Water < public IWaterState State < get; set; >public Water(IWaterState ws) < State = ws; >public void Heat() < State.Heat(this); >public void Frost() < State.Frost(this); >> interface IWaterState < void Heat(Water water); void Frost(Water water); >class SolidWaterState : IWaterState < public void Heat(Water water) < Console.WriteLine("Превращаем лед в жидкость"); water.State = new LiquidWaterState(); >public void Frost(Water water) < Console.WriteLine("Продолжаем заморозку льда"); >> class LiquidWaterState : IWaterState < public void Heat(Water water) < Console.WriteLine("Превращаем жидкость в пар"); water.State = new GasWaterState(); >public void Frost(Water water) < Console.WriteLine("Превращаем жидкость в лед"); water.State = new SolidWaterState(); >> class GasWaterState : IWaterState < public void Heat(Water water) < Console.WriteLine("Повышаем температуру водяного пара"); >public void Frost(Water water) < Console.WriteLine("Превращаем водяной пар в жидкость"); water.State = new LiquidWaterState(); >>Таким образом, реализация паттерна Состояние позволяет вынести поведение, зависящее от текущего состояния объекта, в отдельные классы, и избежать перегруженности методов объекта условными конструкциями, как if..else или switch. Кроме того, при необходимости мы можем ввести в систему новые классы состояний, а имеющиеся классы состояний использовать в других объектах.
nodkz / readme.md
Чтоб вы поняли мою точку зрения, мне придется зайти с далека, чтобы вас ввести в свой контекст. Буду говорить просто и квадратно, и можно зацепиться за любое предложение и опровергнуть его.
Так вот начнем с далека. Человек изобрел транзистор. Из них собрали регистры, логичейские гейты AND, OR, XOR и пошла поехала булевая логика. Еще чутка напряглись и собрали калькуляторы и компьютеры. Поняли, что считает эта штука быстрее человека, и надо туда засовывать всё больше и больше вычислительной логики.
В 50-ых минули времена инструкций и чистого ассемблера, придумали языки программирования высокого уровня, типа Fortran, который позволял людям быстрее писать программы. Завезли туда переменные (VARIABLES) и начали писать безумные алгоритмы вычисления.
Так активно писали, что где-то в 60х запарились и придумали функции (FUNCTIONS), чтоб изолировать/инкапсулировать куски логики и алгоритмов. Привет функциональному подходу из 60х.
В 70x годах в языке Simula появились принципы Объектно Ориентированного Программирования – наши любимые классы и инстансы (CLASS). Просто логика стала настолько активно расти, что пришлось ряд переменных и функции, которые их обслуживают, объединять и инкапсулировать в новую сущность – класс. Привет ООПшникам из 70х.
Где-то в 80-ых стали активно развиваться параллельные вычисления. Народ намучился с синхронизацией состояния. И появился паттерн Наблюдатель (Observable), который позволяет объекту получать оповещения об изменении состояния других объектов.
В 90х появился HTML. А чуть позже появился JavaScript (в 1995). Событие по знаковости, как в 50-ых появились языки программирования высокого уровня. Даешь странички с примитивной типографикой и гиперссылками!
Ах да, и большинство наших зрителей появились тоже в 90х. Привет вам всем огромный.
В нулевых появился Gmail (2004) – первый SPA (Single Page Application). Началась эра веб-клиентов с богатой логикой на своей стороне. Событие по знаковости, как в 60-ых появились функции. Даешь больше логики на клиенте!
В 10ых стали появлятся Angular, Ember, React и прочие. Событие по знаковости, как в 70-ых появились классы. Стока много кода и логики, надо все объединять, собирать и структурировать. Давай фреймворки в браузеры!
Ну и к 20 году мы с вами заговорили о Стейт Менеджерах. Событие по знаковости, как в 80-ых появились параллельные вычисления и паттерн Observable. Мы замучались синхронизировать состояние в разных частях приложения. Даешь синхронизацию, даешь Стейт Менеджеры!
Т.е. история развивается по спирали. Фронтендеры – это бэкендеры со сдвигом в 40-50 лет, которые смогли начать программировать только в 2000х годах.
Так что такое Стейт Менеджер? Все до банальности просто:
- это одна или несколько переменных из 50х (например адресная строка в браузере)
- набор функции из 60х, либо класс из 70х (например History API, которые позволяет менять адресную строку)
- и паттерн Observable из 80х (например event handler window.onpopstate) Всё это взяли объединили и получили BrowserHistory (который используется к примеру в React Router) — примитивный стейт менеджер, который позволяет управлять браузерной строкой и подписываться на ее изменения.
Итак, джентельменский набор любого стейт менеджера это — набор переменных, набор функций для изменения этих переменных и какой-нибудь Observable, который позволяет вам подписаться на изменения этих самых переменных.
И всякие там истории предыдущих состояний, иммутабельность, реактивность, потоки, EventEmitter, декораторы, thunk’и, хождение по сети за данными – это все сахар, ну или разновидности имплементации стейт менеджеров.
Переменная + Функция + паттерн Observable = вот оно мое базовое определение Стейт Менеджера. Ну а сверху уже добавляются разный примочки под определенные задачи.