Pattern State C# | Паттерн Состояние C#
Давайте рассмотрим паттерн проектирования Состояние C#, для чего он нужен и какие проблемы он решает. Где можно применять данный шаблон, а где это будет излишним.
Идея паттерна проектирования Состояние C#
Паттерн проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте подробнее рассмотрим один из наиболее часто используемых поведенческих паттернов — Состояние C# (State C#).
Существует три вида паттернов проектирования:
Состояние (State) — это поведенческий паттерн, который предоставляет возможность экземпляру класса самостоятельно регулировать свое поведение, ориентируясь на его текущее внутреннем статусе. То есть, при изменении каких-либо внутренних значений класс может кардинально изменять свое поведение.
Архитектура паттерна Состояние
Давайте рассмотрим UML диаграмму данного поведенческого паттерна
Логика работы шаблона Состояние (State)
Сразу перейдем к рассмотрению примера из одной из возможных предметных областей, где применение данного паттерна можно считать уместным. Возьмем обычную кредитную банковскую карту. На ней хранятся два тип средств, это собственные средства, которые были зачислены пользователю его работодателем, и кредитные средства, которые принадлежат банку, но при необходимости пользователь может ими воспользоваться, при условии возврата. Исходя из этого, мы можем выделить три состояния кредитной карты: блокировка (денег нет совсем), расходование кредитных средств, расходование собственных средств. При этом, если на карточке в первую очередь расходуются собственные средства, а пополняются в первую очередь кредитные. Так же мы можем выделить две операции, это пополнение, и расходование средств.
Теперь, когда мы выделили возможные состояния и операции, нам необходимо определить возможные направления перехода между состояниями. Это проще всего сделать с помощью схемы.
Как можно понять из схемы, если на карте имеются собственные средства, то при их исчерпании начинают расходоваться кредитные. Если заканчиваются и кредитные, то карточка блокируется до пополнения средств. При пополнении счета, накопление собственных средств не начинается до тех пор, пока ни будет полностью погашена кредитная задолженность. Если кредитные средства имеются в полном объеме, то при пополнении увеличивается запас собственных средств.
При этом в соответствии с логикой паттерна мы будем всегда вызывать одни и те же методы пополнения и снятия средств, а кредитная карта сама будет изменять свое поведение в зависимости от текущего состояния счета.
Реализация паттерна проектирования Состояние (State) на языке C#
Для начала определим интерфейс состояний. Любое состояние должно реализовывать две операции: снятие и пополнение счета.
IState.cs
Теперь реализуем сами конкретные состояния кредитной карты:
Blocked.cs
UsingCreditFunds.cs
UsingOwnFunds.cs
Теперь реализуем сам класс кредитной карты.
Card.cs
Ну и наконец начнем работать с нашей кредитной картой.
Program.cs
В результате получаем следующий вывод на консоль
Исходный код доступен в репозитории github.
Советую также изучить статью Паттерн проектирования Стратегия (Strategy) на языке C#. А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.
Похожее
Похожие записи
Lazy command C# | Паттерн Ленивая команда C#
Состояние
Состояние — это поведенческий паттерн проектирования, который позволяет объектам менять поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.
Паттерн Состояние невозможно рассматривать в отрыве от концепции машины состояний, также известной как стейт-машина или конечный автомат.
Основная идея в том, что программа может находиться в одном из нескольких состояний, которые всё время сменяют друг друга. Набор этих состояний, а также переходов между ними, предопределён и конечен. Находясь в разных состояниях, программа может по-разному реагировать на одни и те же события, которые происходят с ней.
Возможные состояния документа и переходы между ними.
Основная проблема такой машины состояний проявится в том случае, если в Документ добавить ещё десяток состояний. Каждый метод будет состоять из увесистого условного оператора, перебирающего доступные состояния. Такой код крайне сложно поддерживать. Малейшее изменение логики переходов заставит вас перепроверять работу всех методов, которые содержат условные операторы машины состояний.
Путаница и нагромождение условий особенно сильно проявляется в старых проектах. Набор возможных состояний бывает трудно предопределить заранее, поэтому они всё время добавляются в процессе эволюции программы. Из-за этого решение, которое выглядело простым и эффективным в самом начале разработки, может впоследствии стать проекцией большого макаронного монстра.
Паттерн Состояние предлагает создать отдельные классы для каждого состояния, в котором может пребывать объект, а затем вынести туда поведения, соответствующие этим состояниям.
Вместо того, чтобы хранить код всех состояний, первоначальный объект, называемый контекстом, будет содержать ссылку на один из объектов-состояний и делегировать ему работу, зависящую от состояния.
Документ делегирует работу своему активному объекту-состоянию.
Благодаря тому, что объекты состояний будут иметь общий интерфейс, контекст сможет делегировать работу состоянию, не привязываясь к его классу. Поведение контекста можно будет изменить в любой момент, подключив к нему другой объект-состояние.
Очень важным нюансом, отличающим этот паттерн от Стратегии, является то, что и контекст, и сами конкретные состояния могут знать друг о друге и инициировать переходы от одного состояния к другому.
Ваш смартфон ведёт себя по-разному, в зависимости от текущего состояния:

Контекст хранит ссылку на объект состояния и делегирует ему часть работы, зависящей от состояний. Контекст работает с этим объектом через общий интерфейс состояний. Контекст должен иметь метод для присваивания ему нового объекта-состояния.
Состояние описывает общий интерфейс для всех конкретных состояний.
Конкретные состояния реализуют поведения, связанные с определённым состоянием контекста. Иногда приходится создавать целые иерархии классов состояний, чтобы обобщить дублирующий код.
Состояние может иметь обратную ссылку на объект контекста. Через неё не только удобно получать из контекста нужную информацию, но и осуществлять смену его состояния.
И контекст, и объекты конкретных состояний могут решать, когда и какое следующее состояние будет выбрано. Чтобы переключить состояние, нужно подать другой объект-состояние в контекст.
В этом примере паттерн Состояние изменяет функциональность одних и тех же элементов управления музыкальным проигрывателем, в зависимости от того, в каком состоянии находится сейчас проигрыватель.
Пример изменение поведения проигрывателя с помощью состояний.
Объект проигрывателя содержит объект-состояние, которому и делегирует основную работу. Изменяя состояния, можно менять то, как ведут себя элементы управления проигрывателя.
Когда у вас есть объект, поведение которого кардинально меняется в зависимости от внутреннего состояния, причём типов состояний много, и их код часто меняется.
Паттерн предлагает выделить в собственные классы все поля и методы, связанные с определёнными состояниями. Первоначальный объект будет постоянно ссылаться на один из объектов-состояний, делегируя ему часть своей работы. Для изменения состояния в контекст достаточно будет подставить другой объект-состояние.
Когда код класса содержит множество больших, похожих друг на друга, условных операторов, которые выбирают поведения в зависимости от текущих значений полей класса.
Паттерн предлагает переместить каждую ветку такого условного оператора в собственный класс. Тут же можно поселить и все поля, связанные с данным состоянием.
Когда вы сознательно используете табличную машину состояний, построенную на условных операторах, но вынуждены мириться с дублированием кода для похожих состояний и переходов.
Паттерн Состояние позволяет реализовать иерархическую машину состояний, базирующуюся на наследовании. Вы можете отнаследовать похожие состояния от одного родительского класса и вынести туда весь дублирующий код.
Определитесь с классом, который будет играть роль контекста. Это может быть как существующий класс, в котором уже есть зависимость от состояния, так и новый класс, если код состояний размазан по нескольким классам.
Создайте общий интерфейс состояний. Он должен описывать методы, общие для всех состояний, обнаруженных в контексте. Заметьте, что не всё поведение контекста нужно переносить в состояние, а только то, которое зависит от состояний.
Для каждого фактического состояния создайте класс, реализующий интерфейс состояния. Переместите код, связанный с конкретными состояниями в нужные классы. В конце концов, все методы интерфейса состояния должны быть реализованы во всех классах состояний.
При переносе поведения из контекста вы можете столкнуться с тем, что это поведение зависит от приватных полей или методов контекста, к которым нет доступа из объекта состояния. Существует парочка способов обойти эту проблему.
Самый простой — оставить поведение внутри контекста, вызывая его из объекта состояния. С другой стороны, вы можете сделать классы состояний вложенными в класс контекста, и тогда они получат доступ ко всем приватным частям контекста. Но последний способ доступен только в некоторых языках программирования (например, Java, C#).
Создайте в контексте поле для хранения объектов-состояний, а также публичный метод для изменения значения этого поля.
Старые методы контекста, в которых находился зависимый от состояния код, замените на вызовы соответствующих методов объекта-состояния.
В зависимости от бизнес-логики, разместите код, который переключает состояние контекста либо внутри контекста, либо внутри классов конкретных состояний.
- Избавляет от множества больших условных операторов машины состояний. Концентрирует в одном месте код, связанный с определённым состоянием. Упрощает код контекста.
- Может неоправданно усложнить код, если состояний мало и они редко меняются.
Мост, Стратегия и Состояние (а также слегка и Адаптер) имеют схожие структуры классов — все они построены на принципе «композиции», то есть делегирования работы другим объектам. Тем не менее, они отличаются тем, что решают разные проблемы. Помните, что паттерны — это не только рецепт построения кода определённым образом, но и описание проблем, которые привели к данному решению.
Состояние можно рассматривать как надстройку над Стратегией. Оба паттерна используют композицию, чтобы менять поведение основного объекта, делегируя работу вложенным объектам-помощникам. Однако в Стратегии эти объекты не знают друг о друге и никак не связаны. В Состоянии сами конкретные состояния могут переключать контекст.
Не втыкай в транспорте
Лучше почитай нашу книгу о паттернах проектирования.
Теперь это удобно делать даже во время поездок в общественном транспорте.
Эта статья является частью нашей электронной книги Погружение в Паттерны Проектирования.
Простые стейт-машины на службе у разработчика
Представьте на минутку обычного программиста. Допустим, его зовут Вася и ему нужно сделать анимированную менюшку на сайт/десктоп приложение/мобильный апп. Знаете, которые выезжают сверху вниз, как меню у окна Windows или меню с яблочком у OS X. Вот такое.
Начинает он с одного выпадающего окошка, тестирует анимацию, выставляет ease out 100% и наслаждается полученным результатом. Но вскоре он понимает, что для того, чтобы управлять менюшкой, хорошо бы знать закрыто оно сейчас или нет. Мы-то с вами тут программисты опытные, все понимаем, что нужно добавить флаг. Не вопрос, флаг есть.
Вроде, работает. Но, если быстро кликать по кнопке, меню начинает моргать, открываясь и закрываясь не успев доанимироваться в конечное состояние. Вася добавляет флаг animating. Теперь код у нас такой:
Через какое-то время Васе говорят, что меню может быть полностью выключено и неактивно. Не вопрос! Мы-то с вами тут программисты опытные, все понимаем, что… нужно добавить ЕЩЕ ОДИН ФЛАГ! И, всего-то через пару дней разработки, код меню уже пестрит двустрочными IF-ами типа вот такого:
Вася начинает задаваться вопросами: как вообще может быть, что animating == true и enabled == false; почему у него время от времени все глючит; как тут вообще поймешь в каком состоянии находится меню. Ага! Состояния. О них дальше и пойдет речь.
Знакомьтесь, это Вася.

Состояние
Вася уже начинает понимать, что многие комбинации флагов не имеют смысла, а остальные можно легко описать парой слов, например: Disabled, Idle, Animating, Opened. Все мы тут программисты опытные, сразу вспоминаем про state machines. Но, для Васи придется рассказать что это и зачем. Простым языком, без всяких математических терминов.
У нас есть объект, например, вышеупомянутая менюшка. Объект всегда находится в каком-то одном состоянии и реагируя на различные события может между этими состояниями переходить. Обычно состояния, события и переходы удобно описывать вот такими схемами (кружочками обозначены начальное и конечные состояния):
Из схемы понятно, что из состояния Inactive в Active можно попасть только по событию Begin, а из состояния Paused можно попасть как и в Active, так и в Inactive. Такую простую концепцию почему-то называют «Конечный Автомат» или «Finite State Machine», что очень пугает обычных людей.
По завету ООП, состояния должны быть скрыты внутри объекта и просто так снаружи не доступны. Например, у объекта во время работы может быть 20 разных состояний, но внешнее API на вопрос «чо как дела?» отвечает «ничо так» на 19 из них и только на 1 ругается матом, что проср*ли все полимеры.
Следуя концепции стейт машин, очень легко структурировать код так, что всегда будет ясно что и как делает тот или иной объект. Всегда будет понятно, что что-то пошло не так, если система вдруг попыталась перейти в недоступное из данного состояния состояние. А события, которые вдруг посмели прийти в неправильное время, можно смело игнорировать и не бояться, что что-нибудь сломается.
Самая простая в мире стейт машина
Допустим, теперь Вася делает проект на C# и ему нужна простая стейт машина для одного типа объектов. Он пишет что-то типа такого:
А вот так обрабатывает события в зависимости от текущего состояния:
Но, мы-то с вами тут программисты опытные, все понимаем, что метод setState в итоге разрастется на пару десятков страниц, что (как написано в учебниках) не есть хорошо.
State Pattern
Погуглив пару часов, Вася решает, что State Pattern идеально подходит в данной ситуации. Тем более, что старшие программисты все время соревнуются кто больше паттернов запихнет в свой апп, так что, решает Вася, паттерны это дело важное.
Например, для State Pattern можно сделать интерфейс IState:
И по отдельному классу для каждого состояния, которые этот интерфейс имплементят. В теории выглядит красиво и 100% по учебнику.
Но, во-первых, для каждой несчастной мелкой стейт машины нужно городить уйму классов, что само по себе небыстро. Во-вторых, рано или поздно начнутся проблемы с доступом к общим данным. Где их хранить? В основном классе? А как классы-состояния получат к ним доступ? А как мне тут за 15 минут перед дедлайном впилить быстро мелкий хак в обход правил? И подобные проблемы взаимодействия, которые будут сильно тормозить разработку.
Реализация на основе особенностей языка
Некоторые языки программирования облегчают решение тех или иных задач. В Ruby, например, так вообще есть целый DSL (и не один) для создания конечных автоматов. А в C# конечный автомат можно упростить через Reflection. Вот как-то так:
Реализовав систему описанную выше, Вася понимает, что у нее тоже больше минусов, чем плюсов:
Фреймворк
А тем временем, Вася уже вовсю стал вникать в теорию стейт машин и решил, что хорошо бы иметь возможность формально их описывать через API или (о Боже) через XML, что в теории звучит круто. Мы-то с вами тут программисты опытные, все понимаем, что нужно писать свой фреймворк. Потому что другие не подходят, так как у всех у них есть один фатальный недостаток.
Вася решил, что с помощью его фреймворка можно будет быстро и легко создать стейт машину без необходимости писать много ненужного кода. Фреймворк не будет накладывать никаких ограничений на разработчика. Все вокруг будут веселы и жизнерадостны.
Я попробовал множество фреймворков на разных языках, несколько подобных написал сам. И всегда для описания конечного автомата средствами фреймворка требовалось больше кода, чем в простом примере. Все они накладывают те или иные ограничения, а многие пытаются делать сразу столько всего, что для того, чтобы разобраться, как же тут все-таки создать несложную стейт машину, приходится продолжительное время рыться в документации.
Вот, например, описание конечного автомата фреймворком stateless:
Но, пробившись через создание стейт машины, можно воспользоваться полезными функциями, которые предоставляет фреймворк. В основном это: проверка правильности переходов, синхронизация зависимых стейт машин и суб-стейт машин и всяческая защита от дурака.
XML — это отдельное зло. Кто-то когда-то придумал использовать его для написания конфигов. Стадо леммингов java разработчиков длительное время молилось на него. А теперь никто уже и не знает зачем все используют XML, но продолжают бить всех, кто пытается от него избавиться.
Вася тоже загорелся идеей, что можно все сконфигурировать в XML и НЕ ПИСАТЬ НИ СТРОЧКИ КОДА! В итоге в его фреймворке отдельно лежат XML файлы примерно такого содержания:
Класс! И никакого программирования. Но, мы-то с вами тут программисты опытные, все понимаем, что программирование никуда не ушло. Вася заменил кусок императивного кода на кусок декларативного кода, добавив при этом во фреймворк интерпретатор XML, который все еще в пару раз усложнил. А потом попробуй это отдебажить, когда код на разных языках и разбросан по проекту.
Соглашение
И тут Васе все это надоело и он вернулся обратно к самому простому в мире конечному автомату. Он его немного переделал и придумал правила как писать в нем код.
UPDATE: спасибо за комментарии. Здесь действительно не хватало небольшого объяснения.
У нас есть несколько состояний. Переход между ними — это транзакция из атомарных операций, то есть они все происходят всегда вместе, в правильном порядке и между ними не может вклиниться еще какой-то код. При смене состояния с A на B происходит следующее: выполняется код выхода из состояния A, состояние меняется с A на B, выполняется код входа в состояние B.
Для перехода на состояние A нужно вызвать метод stateA, который выполнит нужную логику и вызовет setState(A). Самому вызывать setState(A) крайне не рекомендуется.
UPDATE: В setState() пишется уникальная логика выхода из состояния, а в stateB() возможна специфическая логика выхода из состояния A при переходе в B. Но очень редко используется.
Простое соглашение для написания стейт машин. Оно достаточно гибкое и имеет следующие плюсы:
Как и во всех соглашениях, какой-то код может сперва находиться в одном месте, но потом у него появится другой смысл, или окажется, что он где-то дублируется. Тогда мы можем его перенести в другое место. Никто нам не запрещает. Все-таки код не вытесан из камня, это всего лишь текст, который (о ужас!) можно и нужно менять с развитием проекта.
UPDATE: а setState() вполне можно заменить одним сеттером для наглядности.
Заключение
На этом заканчивается увлекательное приключение Васи в мире стейт машин. А ведь впереди еще столько всего интересного. Отдельного топика бы только заслужили параллельные и зависимые стейт машины.
Я надеюсь, что, если вы еще не используете стейт машины повсеместно, эта статья перетянет вас на сторону добра; если вы пишите свой уберфреймворк для работы со стейт машинами, она поможет свежим взглядом посмотреть на то, что у вас получается.
Я надеюсь, что эта статья поможет разработчикам задуматься где и когда стоит использовать паттерны и фреймворки, и что описанное соглашение по оформлению стейт машин окажется кому-то полезным.
Паттерн Состояние (State) — JS: Полиморфизм
Паттерн «Состояние» – яркий пример замены условных конструкций на полиморфизм подтипов. Он довольно широко используется и способен по-настоящему снизить сложность кода. Разберём его на примере поведения экранов телефонов.
Не все телефоны ведут себя одинаковым образом, но для урока надо было выбрать конкретный пример
Всего у телефона три базовых состояния:
Смоделируем эту логику в классе, отвечающем за экран, и добавим туда два события: прикосновение (touch) и смахивание (swipe).
Событий всего два, а уже сколько условных конструкций. В реальности событий было бы гораздо больше, и все они должны учитывать состояние активности телефона и экрана.
Решая эту задачу в лоб, мы получим огромное количество условных конструкций в методе каждого события. Такой код очень сложен и хрупок. Изменение количества состояний и добавление новых событий чревато постоянными багами. Тяжело увидеть картину целиком и что-то не упустить.
Сложность такого кода можно значительно снизить за счет двух последовательных преобразований: выделения явного состояния и подключения полиморфизма подтипов.
Явно выделенное состояние
Текущая реализация экрана опирается на флаги. В программировании так называют переменные содержащие булевы значения.
Такой стиль программирования имеет свое название: «флаговое программирование». Так говорят про код, в котором трудно разобраться из-за наличия логики, завязанной на комбинацию флагов. А наличие флагов почти наверняка к этому приведет. Все дело в том, что количество состояний у систем, как правило, больше чем два. То есть одного флага никогда не будет достаточно.
От флагов возможно уйти, введя явное состояние системы. В нашем примере несложно заметить, что состояний всего три:
Следующий шаг, заменить флаги на одну переменную, которая хранит текущее состояние системы:
Главное что произошло в коде выше – пропали проверки на комбинацию флагов. Это не отменяет возможности проверок сразу по нескольким состояниям, но состояния системы понимать гораздо проще, чем наборы флагов.
Классы Состояний
Для избавления от условных конструкций понадобится полиморфизм. На базе чего его строить? Благодаря наличию явно выделенного состояния легко увидеть зависимость поведения от состояния. Именно состояния должны трансформироваться в классы со своим собственным поведением, специфичным для данного состояния.
Экран, в свою очередь, избавится от всех проверок и начнет взаимодействовать с состояниями:
Теперь экран не делает ровным счетом ничего. Весь его код — это инициализация начального состояния и передача управления текущему активному состоянию. Как же выглядят классы состояний?
Проще всех устроено состояние выключенного телефона. В этом состоянии нет никакой реакции, поэтому методы пустые. Посмотрим ScreenDisabledState :







