→ Паттерн состояние c#. Шаблон проектирования «состояние» двадцать лет спустя. UML-диаграмма классов паттерна State

Паттерн состояние c#. Шаблон проектирования «состояние» двадцать лет спустя. UML-диаграмма классов паттерна State

«Паттерн State» .ru источник

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

Условия, Задача, Назначение

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

Мотивация

Рассмотрим класс TCPConnection , с помощью которого представлено сетевое соединение. Объект этого класса может находиться в одном из нескольких состояний: Established (установлено), Listening (прослушивание), Closed (закрыто). Когда объект TCPConnection получает запросы от других объектов, то в зависимости от текущего состояния он отвечает по-разному. Например, ответ на запрос Open (открыть) зависит от того, находится ли соединение в состоянии Closed или Established . Паттерн состояние описывает, каким образом объект TCPConnection может вести себя по-разному, находясь в различных состояниях. источник.ru

Основная идея этого паттерна заключается в том, чтобы ввести абстрактный класс TCPState для представления различных состояний соединения. Этот класс объявляет интерфейс, единый для всех классов, описывающих различные рабочие источник.ru

состояния. В этих подклассах TCPState реализуется поведение, специфичное для конкретного состояния. Например, в классах TCPEstablished и TCPClosed реализовано поведение, характерное для состояний Established и Closed соответственно. сайт сайт оригинал источник

сайт оригинал источник сайт

Класс TCPConnection хранит у себя объект состояния (экземпляр подкласса TCPState ), представляющий текущее состояние соединения, и делегирует все зависящие от состояния запросы этому объекту. TCPConnection использует свой экземпляр подкласса TCPState достаточно просто: вызывая методы единого интерфейса TCPState , только в зависимости от того какой в данный момент хранится конкретный подкласс TCPState -а - результат получается разным, т.е. в реальности выполняются операции, свойственные только данному состоянию соединения. оригинал.ru источник

А при каждом изменении состояния соединения TCPConnection изменяет свой объект-состояние. Например, когда установленное соединение закрывается, TCPConnection заменяет экземпляр класса TCPEstablished экземпляром TCPClosed . сайт оригинал источник сайт

Признаки применения, использования паттерна Состояние (State)

Используйте паттерн состояние в следующих случаях: источник оригинал.ru
  1. Когда поведение объекта зависит от его состояния и при этом должно изменяться во время выполнения. .ru
  2. Когда в коде операций встречаются состоящие из многих ветвей условные операторы, в которых выбор ветви зависит от состояния. Обычно в таком случае состояние представлено перечисляемыми константами. Часто одна и та же структура условного оператора повторяется в нескольких операциях.Паттерн состояние предлагает поместить каждую ветвь в отдельный класс. Это позволяет трактовать состояние объекта как самостоятельный объект, который может изменяться независимо от других. источник оригинал.ru

Решение

сайт источник сайт оригинал

оригинал.ru

Участники паттерна Состояние (State)

источник оригинал.ru
  1. Context (TCPConnection) - контекст.
    Определяет единый интерфейс для клиентов.
    Хранит экземпляр подкласса ConcreteState , которым определяется текущее состояние. оригинал.ru
  2. State (TCPState) - состояние.
    Определяет интерфейс для инкапсуляции поведения, ассоциированного с конкретным состоянием контекста Context. источник оригинал.ru
  3. Подклассы ConcreteState (TCPEstablished, TCPListen, TCPClosed) - конкретное состояние.
    Каждый подкласс реализует поведение, ассоциированное с некоторым состоянием контекста Context . сайт сайт оригинал источник

Схема использования паттерна Состояние (State)

Класс Context делегирует запросы текущему объекту ConcreteState . сайт сайт оригинал источник

Контекст может передать себя в качестве аргумента объекту State , который будет обрабатывать запрос. Это дает возможность объекту-состоянию (ConcreteState ) при необходимости получить доступ к контексту. сайт оригинал источник сайт

Context - это основной интерфейс для клиентов. Клиенты могут конфигурировать контекст объектами состояния State (точнее ConcreteState ). Один раз сконфигурировав контекст, клиенты уже не должны напрямую связываться с объектами состояния (только через общий интерфейс State ). сайт источник сайт оригинал

При этом либо Context , либо сами подклассы ConcreteState могут решить, при каких условиях и в каком порядке происходит смена состояний. .ru источник

Вопросы, касающиеся реализации паттерна Состояние (State)

Вопросы, касающиеся реализации паттерна State: оригинал.ru источник
  1. Что определяет переходы между состояниями.
    Паттерн состояние ничего не сообщает о том, какой участник определяет условия (критерии) перехода между состояниями. Если критерии зафиксированы, то их можно реализовать непосредственно в классе Context . Однако в общем случае более гибкий и правильный подход заключается в том, чтобы позволить самим подклассам класса State определять следующее состояние и момент перехода. Для этого в класс Context надо добавить интерфейс, позволяющий из объектов State установить его состояние.
    Такую децентрализованную логику переходов проще модифицировать и расширять - нужно лишь определить новые подклассы State . Недостаток децентрализации в том, что каждый подкласс State должен «знать» еще хотя бы об одном подклассе другого состояния (на которое собственно он и сможет переключить текущее состояние), что вносит реализационные зависимости между подклассами. источник.ru

    сайт источник сайт оригинал
  2. Табличная альтернатива.
    Существует еще один способ структурирования кода, управляемого сменой состояний. Это принцип конечного автомата. Он использует таблицу для отображения входных данных на переходы между состояниями. С ее помощью можно определить, в какое состояние нужно перейти при поступлении некоторых входных данных. По существу, тем самым мы заменяем условный код поиском в таблице.
    Основное преимущество автомата - в его регулярности: для изменения критериев перехода достаточно модифицировать только данные, а не код. Но есть и недостатки:
    - поиск в таблице часто менее эффективен, чем вызов функции,
    - представление логики переходов в однородном табличном формате делает критерии менее явными и, стало быть, более сложными для понимания,
    - обычно трудно добавить действия, которыми сопровождаются переходы между состояниями. Табличный метод учитывает состояния и переходы между ними, но его необходимо дополнить, чтобы при каждом изменении состоянии можно было выполнять произвольные вычисления.
    Главное различие между конечными автоматами на базе таблиц и Паттерн состояние можно сформулировать так: Паттерн состояние моделирует поведение, зависящее от состояния, а табличный метод акцентирует внимание на определении переходов между состояниями. оригинал.ru источник

    источник.ru оригинал
  3. Создание и уничтожение объектов состояния.
    В процессе разработки обычно приходится выбирать между:
    - созданием объектов состояния, когда в них возникает необходимость, и уничтожением сразу после использования,
    - созданием их заранее и навсегда.

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

    сайт источник сайт оригинал
  4. Использование динамического изменения.
    Варьировать поведение по запросу можно, меняя класс объекта во время выполнения, но в большинстве объектно-ориентированных языков это не поддерживается. Исключение составляет Perl, JavaScript и другие основанные на скриптовом движке языки, которые предоставляют такой механизм и, следовательно, поддерживают Паттерн состояние напрямую. Это позволяет объектам варьировать поведение путем изменения кода своего класса. источник.ru оригинал

    оригинал источник.ru

Результаты

Результаты использования паттерна состояние : оригинал.ru источник
  1. Локализует зависящее от состояния поведение.
    И делит его на части, соответствующие состояниям. Паттерн состояние помещает все поведение, ассоциированное с конкретным состоянием, в отдельный объект. Поскольку зависящий от состояния код целиком находится в одном из подклассов класса State , то добавлять новые состояния и переходы можно просто путем порождения новых подклассов.
    Вместо этого можно было бы использовать данные-члены для определения внутренних состояний, тогда операции объекта Context проверяли бы эти данные. Но в таком случае похожие условные операторы или операторы ветвления были бы разбросаны по всему коду класса Context . При этом добавление нового состояния потребовало бы изменения нескольких операций, что затруднило бы сопровождение. Паттерн состояние позволяет решить эту проблему, но одновременно порождает другую, поскольку поведение для различных состояний оказывается распределенным между несколькими подклассами State . Это увеличивает число классов. Конечно, один класс компактнее, но если состояний много, то такое распределение эффективнее, так как в противном случае пришлось бы иметь дело с громоздкими условными операторами.
    Наличие громоздких условных операторов нежелательно, равно как и наличие длинных процедур. Они слишком монолитны, вот почему модификация и расширение кода становится проблемой. Паттерн состояние предлагает более удачный способ структурирования зависящего от состояния кода. Логика, описывающая переходы между состояниями, больше не заключена в монолитные операторы if или switch , а распределена между подклассами State . При инкапсуляции каждого перехода и действия в класс - состояние становится полноценным объектом. Это улучшает структуру кода и проясняет его назначение. источник оригинал.ru
  2. Делает явными переходы между состояниями.
    Если объект определяет свое текущее состояние исключительно в терминах внутренних данных, то переходы между состояниями не имеют явного представления; они проявляются лишь как присваивания некоторым переменным. Ввод отдельных объектов для различных состояний делает переходы более явными. Кроме того, объекты State могут защитить контекст Context от рассогласования внутренних переменных, поскольку переходы с точки зрения контекста - это атомарные действия. Для осуществления перехода надо изменить значение только одной переменной (объектной переменной State в классе Context ), а не нескольких. оригинал.ru источник
  3. Объекты состояния можно разделять.
    Если в объекте состояния State отсутствуют переменные экземпляра, то есть представляемое им состояние кодируется исключительно самим типом, то разные контексты могут разделять один и тот же объект State . Когда состояния разделяются таким образом, они являются, по сути дела, приспособленцами (см. паттерн-приспособленец), у которых нет внутреннего состояния, а есть только поведение. сайт источник оригинал сайт

Пример

Рассмотрим реализацию примера из раздела « », т.е. построение некоторой простенькой архитектуры TCP соединения. Это упрощенный вариант протокола TCP, в нем, конечно же, представлен не весь протокол и даже не все состояния TCP-соединений. сайт оригинал сайт источник

Прежде всего определим класс TCPConnection , который предоставляет интерфейс для передачи данных и обрабатывает запросы на изменение состояния: TCPConnection . источник.ru оригинал

В переменной-члене state класса TCPConnection хранится экземпляр класса TCPState . Этот класс дублирует интерфейс изменения состояния, определенный в классе TCPConnection . сайт источник оригинал сайт

Источник оригинал.ru

TCPConnection делегирует все зависящие от состояния запросы хранимому в state экземпляру TCPState . Кроме того, в классе TCPConnection существует операция ChangeState , с помощью которой в эту переменную можно записать указатель на другой объект TCPState . Конструктор класса TCPConnection инициализирует state указателем на состояние-закрытия TCPClosed (мы определим его ниже). источник.ru

сайт источник оригинал сайт

Каждая операция TCPState принимает экземпляр TCPConnection как параметр, тем самым, позволяя объекту TCPState получить доступ к данным объекта TCPConnection и изменить состояние соединения. .ru

В классе TCPState реализовано поведение по умолчанию для всех делегированных ему запросов. Он может также изменить состояние объекта TCPConnection посредством операции ChangeState . TCPState располагается в том же пакете, что и TCPConnection , поэтому также имеет доступ к этой операции: TCPState . сайт сайт оригинал источник

источник.ru

В подклассах TCPState реализовано поведение, зависящее от состояния. Соединение TCP может находиться во многих состояниях: Established (установлено), Listening (прослушивание), Closed (закрыто) и т.д., и для каждого из них есть свой подкласс TCPState . Для простоты подробно рассмотрим лишь 3 подкласса - TCPEstablished , TCPListen и TCPClosed . сайт сайт источник оригинал

оригинал источник.ru

В подклассах TCPState реализуется зависящее от состояния поведение для тех запросов, которые допустимы в этом состоянии. оригинал.ru источник

сайт оригинал сайт источник

После выполнения специфичных для своего состояния действий эти операции сайт оригинал источник сайт

вызывают ChangeState для изменения состояния объекта TCPConnection . У него же самого нет никакой информации о протоколе TCP. Именно подклассы TCPState определяют переходы между состояниями и действия, диктуемые протоколом. сайт сайт оригинал источник

Ru оригинал

Известные применения паттерна Состояние (State)

Ральф Джонсон и Джонатан Цвейг характеризуют паттерн состояние и описывают его применительно к протоколу TCP.
Наиболее популярные интерактивные программы рисования предоставляют «инструменты» для выполнения операций прямым манипулированием. Например, инструмент для рисования линий позволяет пользователю щелкнуть в произвольной точке мышью, а затем, перемещая мышь, провести из этой точки линию. Инструмент для выбора позволяет выбирать некоторые фигуры. Обычно все имеющиеся инструменты размещаются в палитре. Работа пользователя заключается в том, чтобы выбрать и применить инструмент, но на самом деле поведение редактора варьируется при смене инструмента: посредством инструмента для рисования мы создаем фигуры, при помощи инструмента выбора - выбираем их и т.д. оригинал.ru источник

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

Можно определить абстрактный класс Tool , подклассы которого реализуют зависящее от инструмента поведение. Графический редактор хранит ссылку на текущий объект Too l и делегирует ему поступающие запросы. При выборе инструмента редактор использует другой объект, что приводит к изменению поведения. источник.ru

Данная техника используется в каркасах графических редакторов HotDraw и Unidraw. Она позволяет клиентам легко определять новые виды инструментов. В HotDraw класс DrawingController переадресует запросы текущему объекту Tool . В Unidraw соответствующие классы называются Viewer и Tool . На приведенной ниже диаграмме классов схематично представлены интерфейсы классов Tool

сайт источник сайт оригинал

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

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

Нарушение принципа подстановки Лисков на лицо. Yaron Minsky предложил альтернативный подход : сделайте недопустимые состояния непредставимыми (make illegal states unrepresentable) . Это дает возможность перенести проверку ошибок со времени исполнения на время компиляции. Однако control flow в этом случае будет организован на основе сопоставления с образцом, а не с помощью полиморфизма. К счастью, частичная поддержка pattern matching появилась в C#7 .

Более подробно на примере F# тема make illegal states unrepresentable раскрыта на сайте Скотта Влашина .

Рассмотрим реализацию «состояния» на примере корзины. В C# нет встроенного типа union . Разделим данные и поведение. Само состояние будем кодировать с помощью enum, а поведение отдельным классом. Для удобства объявим атрибут, связывающий enum и соответствующий класс поведения, базовый класс «состояния» и допишем метод расширения для перехода от enum к классу поведения.

Инфраструктура

public class StateAttribute: Attribute { public Type StateType { get; } public StateAttribute(Type stateType) { StateType = stateType ?? throw new ArgumentNullException(nameof(stateType)); } } public abstract class State where T: class { protected State(T entity) { Entity = entity ?? throw new ArgumentNullException(nameof(entity)); } protected T Entity { get; } } public static class StateCodeExtensions { public static State ToState(this Enum stateCode, object entity) where T: class // да, да reflection медленный. Замените компилируемыми expression tree // или IL Emit и будет быстро => (State) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute() .StateType, entity); }

Предметная область

Объявим сущность «корзина»:

Public interface IHasState where TEntity: class { TStateCode StateCode { get; } State State { get; } } public partial class Cart: IHasState { public User User { get; protected set; } public CartStateCode StateCode { get; protected set; } public State State => StateCode.ToState(this); public decimal Total { get; protected set; } protected virtual ICollectionProducts { get; set; } = new List(); // ORM Only protected Cart() { } public Cart(User user) { User = user ?? throw new ArgumentNullException(nameof(user)); StateCode = StateCode = CartStateCode.Empty; } public Cart(User user, IEnumerableProducts) : this(user) { StateCode = StateCode = CartStateCode.Empty; foreach (var product in products) { Products.Add(product); } } public Cart(User user, IEnumerableProducts, decimal total) : this(user, products) { if (total <= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
Реализуем по одному классу на каждое состояние корзины: пустую, активную и оплаченную, но не будем объявлять общий интерфейс. Пусть каждое состояние реализует только релевантное поведение. Это не значит, что классы EmptyCartState , ActiveCartState и PaidCartState не могут реализовать один интерфейс. Они могут, но такой интерфейс должен содержать только методы, доступные в каждом состоянии. В нашем случае метод Add доступен в EmptyCartState и ActiveCartState , поэтому можно унаследовать их от абстрактного AddableCartStateBase . Однако, добавлять товары можно только в неоплаченную корзину, поэтому общего интерфейса для всех состояний не будет. Таким образом мы гарантируем отсутствие InvalidOperationException в нашем коде на этапе компиляции.

Public partial class Cart { public enum CartStateCode: byte { Empty, Active, Paid } public interface IAddableCartState { ActiveCartState Add(Product product); IEnumerableProducts { get; } } public interface INotEmptyCartState { IEnumerableProducts { get; } decimal Total { get; } } public abstract class AddableCartState: State, IAddableCartState { protected AddableCartState(Cart entity): base(entity) { } public ActiveCartState Add(Product product) { Entity.Products.Add(product); Entity.StateCode = CartStateCode.Active; return (ActiveCartState)Entity.State; } public IEnumerableProducts => Entity.Products; } public class EmptyCartState: AddableCartState { public EmptyCartState(Cart entity): base(entity) { } } public class ActiveCartState: AddableCartState, INotEmptyCartState { public ActiveCartState(Cart entity): base(entity) { } public PaidCartState Pay(decimal total) { Entity.Total = total; Entity.StateCode = CartStateCode.Paid; return (PaidCartState)Entity.State; } public State Remove(Product product) { Entity.Products.Remove(product); if(!Entity.Products.Any()) { Entity.StateCode = CartStateCode.Empty; } return Entity.State; } public EmptyCartState Clear() { Entity.Products.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State; } public decimal Total => Products.Sum(x => x.Price); } public class PaidCartState: State, INotEmptyCartState { public IEnumerableProducts => Entity.Products; public decimal Total => Entity.Total; public PaidCartState(Cart entity) : base(entity) { } } }
Состояния объявлены вложенными (nested ) классами не случайно. Вложенные классы имеют доступ к защищенным членам класса Cart , а значит нам не придется жертвовать инкапсуляцией сущности для реализации поведения. Чтобы не мусорить в файле класса сущности я разделил объявление на два: Cart.cs и CartStates.cs с помощью ключевого слова partial .

Public ActionResult GetViewResult(State cartState) { switch (cartState) { case Cart.ActiveCartState activeState: return View("Active", activeState); case Cart.EmptyCartState emptyState: return View("Empty", emptyState); case Cart.PaidCartState paidCartState: return View("Paid", paidCartState); default: throw new InvalidOperationException(); } }
В зависимости от состояния корзины будем использовать разные представления. Для пустой корзины выведем сообщение «ваша корзина пуста». В активной корзине будет список товаров, возможность изменить количество товаров и удалить часть из них, кнопка «оформить заказ» и общая сумма покупки.

Оплаченная корзина будет выглядеть также, как и активная, но без возможности что-либо отредактировать. Этот факт можно отметить выделением интерфейса INotEmptyCartState . Таким образом мы не только избавились от нарушения принципа подстановки Лисков, но и применили принцип разделения интерфейса.

Заключение

В прикладном коде мы можем работать по интерфейсным ссылкам IAddableCartState и INotEmptyCartState , чтобы повторно использовать код, отвечающий за добавление товаров в корзину и вывод товаров в корзине. Я считаю, что pattern matching подходит для control flow в C# только когда между типами нет ничего общего. В остальных случаях работа по базовой ссылке удобнее. Аналогичный прием можно применить не только для кодирования поведения сущности, но и для

Состояние - это поведенческий паттерн проектирования, который позволяет объектам менять поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.

Проблема

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


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

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

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

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

Class Document is field state: string // ... method publish() is switch (state) "draft": state = "moderation" break "moderation": if (currentUser.role == "admin") state = "published" break "published": // Do nothing. break // ...

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

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

Решение

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

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


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

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

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

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

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

Структура



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

    Состояние описывает общий интерфейс для всех конкретных состояний.

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

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

Псевдокод

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


Пример изменение поведения проигрывателя с помощью состояний.

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

// Общий интерфейс всех состояний. 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#).

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

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

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

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

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

Назначение паттерна State

  • Паттерн State позволяет объекту изменять свое поведение в зависимости от внутреннего состояния. Создается впечатление, что объект изменил свой класс.
  • Паттерн State является объектно-ориентированной реализацией конечного автомата.

Решаемая проблема

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

Обсуждение паттерна State

Паттерн State решает указанную проблему следующим образом:

  • Вводит класс Context , в котором определяется интерфейс для внешнего мира.
  • Вводит абстрактный класс State .
  • Представляет различные "состояния" конечного автомата в виде подклассов State .
  • В классе Context имеется указатель на текущее состояние, который изменяется при изменении состояния конечного автомата.

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

Существует также альтернативный таблично-ориентированный подход к проектированию конечных автоматов, основанный на использовании таблицы однозначного отображения входных данных на переходы между состояниями. Однако этот подход обладает недостатками: трудно добавить выполнение действий при выполнении переходов. Подход, основанный на использовании паттерна State, для осуществления переходов между состояниями использует код (вместо структур данных), поэтому эти действия легко добавляемы.

Структура паттерна State

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

Паттерн State позволяет объекту изменять свое поведение в зависимости от внутреннего состояния. Похожая картина может наблюдаться в работе торгового автомата. Автоматы могут иметь различные состояния в зависимости от наличия товаров, суммы полученных монет, возможности размена денег и т.д. После того как покупатель выбрал и оплатил товар, возможны следующие ситуации (состояния):

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

Использование паттерна State

  • Определите существующий или создайте новый класс-"обертку" Context , который будет использоваться клиентом в качестве "конечного автомата".
  • Создайте базовый класс State , который повторяет интерфейс класса Context . Каждый метод принимает один дополнительный параметр: экземпляр класса Context . Класс State может определять любое полезное поведение "по умолчанию".
  • Создайте производные от State классы для всех возможных состояний.
  • Класс-"обертка" Context имеет ссылку на объект "текущее состояние".
  • Все полученные от клиента запросы класс Context просто делегирует объекту "текущее состояние", при этом в качестве дополнительного параметра передается адрес объекта Context .
  • Используя этот адрес, в случае необходимости методы класса State могут изменить "текущее состояние" класса Context .

Особенности паттерна State

  • Объекты класса State часто бывают одиночками .
  • Flyweight показывает, как и когда можно разделять объекты State .
  • Паттерн Interpreter может использовать State для определения контекстов при синтаксическом разборе.
  • Паттерны State и Bridge имеют схожие структуры за исключением того, что Bridge допускает иерархию классов-конвертов (аналогов классов-"оберток"), а State-нет. Эти паттерны имеют схожие структуры, но решают разные задачи: State позволяет объекту изменять свое поведение в зависимости от внутреннего состояния, в то время как Bridge разделяет абстракцию от ее реализации так, что их можно изменять независимо друг от друга.
  • Реализация паттерна State основана на паттерне Strategy . Различия заключаются в их назначении.

Реализация паттерна State

Рассмотрим пример конечного автомата с двумя возможными состояниями и двумя событиями.

#include using namespace std; class Machine { class State *current; public: Machine(); void setCurrent(State *s) { current = s; } void on(); void off(); }; class State { public: virtual void on(Machine *m) { cout << " already ON\n"; } virtual void off(Machine *m) { cout << " already OFF\n"; } }; void Machine::on() { current->on(this); } void Machine::off() { current->off(this); } class ON: public State { public: ON() { cout << " ON-ctor "; }; ~ON() { cout << " dtor-ON\n"; }; void off(Machine *m); }; class OFF: public State { public: OFF() { cout << " OFF-ctor "; }; ~OFF() { cout << " dtor-OFF\n"; }; void on(Machine *m) { cout << " going from OFF to ON"; m->setCurrent(new ON()); delete this; } }; void ON::off(Machine *m) { cout << " going from ON to OFF"; m->setCurrent(new OFF()); delete this; } Machine::Machine() { current = new OFF(); cout << "\n"; } int main() { void(Machine:: *ptrs)() = { Machine::off, Machine::on }; Machine fsm; int num; while (1) { cout << "Enter 0/1: "; cin >> num; (fsm. *ptrs)(); } }

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

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

Нарушение принципа подстановки Лисков на лицо. Yaron Minsky предложил альтернативный подход : сделайте недопустимые состояния непредставимыми (make illegal states unrepresentable) . Это дает возможность перенести проверку ошибок со времени исполнения на время компиляции. Однако control flow в этом случае будет организован на основе сопоставления с образцом, а не с помощью полиморфизма. К счастью, .

Более подробно на примере F# тема make illegal states unrepresentable раскрыта на сайте Скотта Влашина .

Рассмотрим реализацию «состояния» на примере корзины. В C# нет встроенного типа union . Разделим данные и поведение. Само состояние будем кодировать с помощью enum, а поведение отдельным классом. Для удобства объявим атрибут, связывающий enum и соответствующий класс поведения, базовый класс «состояния» и допишем метод расширения для перехода от enum к классу поведения.

Инфраструктура

public class StateAttribute: Attribute { public Type StateType { get; } public StateAttribute(Type stateType) { StateType = stateType ?? throw new ArgumentNullException(nameof(stateType)); } } public abstract class State where T: class { protected State(T entity) { Entity = entity ?? throw new ArgumentNullException(nameof(entity)); } protected T Entity { get; } } public static class StateCodeExtensions { public static State ToState(this Enum stateCode, object entity) where T: class // да, да reflection медленный. Замените компилируемыми expression tree // или IL Emit и будет быстро => (State) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute() .StateType, entity); }

Предметная область

Объявим сущность «корзина»:

Public interface IHasState where TEntity: class { TStateCode StateCode { get; } State State { get; } } public partial class Cart: IHasState { public User User { get; protected set; } public CartStateCode StateCode { get; protected set; } public State State => StateCode.ToState(this); public decimal Total { get; protected set; } protected virtual ICollectionProducts { get; set; } = new List(); // ORM Only protected Cart() { } public Cart(User user) { User = user ?? throw new ArgumentNullException(nameof(user)); StateCode = StateCode = CartStateCode.Empty; } public Cart(User user, IEnumerableProducts) : this(user) { StateCode = StateCode = CartStateCode.Empty; foreach (var product in products) { Products.Add(product); } } public Cart(User user, IEnumerableProducts, decimal total) : this(user, products) { if (total <= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
Реализуем по одному классу на каждое состояние корзины: пустую, активную и оплаченную, но не будем объявлять общий интерфейс. Пусть каждое состояние реализует только релевантное поведение. Это не значит, что классы EmptyCartState , ActiveCartState и PaidCartState не могут реализовать один интерфейс. Они могут, но такой интерфейс должен содержать только методы, доступные в каждом состоянии. В нашем случае метод Add доступен в EmptyCartState и ActiveCartState , поэтому можно унаследовать их от абстрактного AddableCartStateBase . Однако, добавлять товары можно только в неоплаченную корзину, поэтому общего интерфейса для всех состояний не будет. Таким образом мы гарантируем отсутствие InvalidOperationException в нашем коде на этапе компиляции.

Public partial class Cart { public enum CartStateCode: byte { Empty, Active, Paid } public interface IAddableCartState { ActiveCartState Add(Product product); IEnumerableProducts { get; } } public interface INotEmptyCartState { IEnumerableProducts { get; } decimal Total { get; } } public abstract class AddableCartState: State, IAddableCartState { protected AddableCartState(Cart entity): base(entity) { } public ActiveCartState Add(Product product) { Entity.Products.Add(product); Entity.StateCode = CartStateCode.Active; return (ActiveCartState)Entity.State; } public IEnumerableProducts => Entity.Products; } public class EmptyCartState: AddableCartState { public EmptyCartState(Cart entity): base(entity) { } } public class ActiveCartState: AddableCartState, INotEmptyCartState { public ActiveCartState(Cart entity): base(entity) { } public PaidCartState Pay(decimal total) { Entity.Total = total; Entity.StateCode = CartStateCode.Paid; return (PaidCartState)Entity.State; } public State Remove(Product product) { Entity.Products.Remove(product); if(!Entity.Products.Any()) { Entity.StateCode = CartStateCode.Empty; } return Entity.State; } public EmptyCartState Clear() { Entity.Products.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State; } public decimal Total => Products.Sum(x => x.Price); } public class PaidCartState: State, INotEmptyCartState { public IEnumerableProducts => Entity.Products; public decimal Total => Entity.Total; public PaidCartState(Cart entity) : base(entity) { } } }
Состояния объявлены вложенными (nested ) классами не случайно. Вложенные классы имеют доступ к защищенным членам класса Cart , а значит нам не придется жертвовать инкапсуляцией сущности для реализации поведения. Чтобы не мусорить в файле класса сущности я разделил объявление на два: Cart.cs и CartStates.cs с помощью ключевого слова partial .

Public ActionResult GetViewResult(State cartState) { switch (cartState) { case Cart.ActiveCartState activeState: return View("Active", activeState); case Cart.EmptyCartState emptyState: return View("Empty", emptyState); case Cart.PaidCartState paidCartState: return View("Paid", paidCartState); default: throw new InvalidOperationException(); } }
В зависимости от состояния корзины будем использовать разные представления. Для пустой корзины выведем сообщение «ваша корзина пуста». В активной корзине будет список товаров, возможность изменить количество товаров и удалить часть из них, кнопка «оформить заказ» и общая сумма покупки.

Оплаченная корзина будет выглядеть также, как и активная, но без возможности что-либо отредактировать. Этот факт можно отметить выделением интерфейса INotEmptyCartState . Таким образом мы не только избавились от нарушения принципа подстановки Лисков, но и применили принцип разделения интерфейса.

Заключение

В прикладном коде мы можем работать по интерфейсным ссылкам IAddableCartState и INotEmptyCartState , чтобы повторно использовать код, отвечающий за добавление товаров в корзину и вывод товаров в корзине. Я считаю, что pattern matching подходит для control flow в C# только когда между типами нет ничего общего. В остальных случаях работа по базовой ссылке удобнее. Аналогичный прием можно применить не только для кодирования поведения сущности, но и для .

 

 

Это интересно: