Состояние. Шаблон проектирования «состояние» двадцать лет спустя А где же эти объекты состояния

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

Казалось бы, все по книжке, но есть нюанс. Как правильно реализовать методы не релевантные для данного состояния? Например, как удалить товар из пустой корзины или оплатить пустую корзину? Обычно каждый 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# только когда между типами нет ничего общего. В остальных случаях работа по базовой ссылке удобнее. Аналогичный прием можно применить не только для кодирования поведения сущности, но и для

Название и классификация паттерна

Состояние - паттерн поведения объектов.

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

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

Паттерн State является объектно-ориентированной реализацией конечного автомата.

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

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

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

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

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

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

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

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

UML-диаграмма классов паттерна State Структура паттерна Состояние показана на рис. 71.

context.setState(StateTwo);

Рис. 71. UML-диаграмма паттерна Состояние

Участники

Context - контекст:

  • определяет интерфейс, представляющий интерес для клиентов;
  • хранит экземпляр подкласса ConcreteState, которым определяется текущее состояние.

State - состояние: определяет интерфейс для инкапсуляции поведения, ассоциированного с конкретным состоянием контекста Context.

Подклассы StateOne, StateTwo, StateThree - конкретное состояние: каждый подкласс реализует поведение, ассоциированное с некоторым состоянием контекста Context.

Отношения

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

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

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

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

Пример паттерна State

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

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

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

Определите существующий или создайте новый класс-«обертку» Context, который будет использоваться клиентом в качестве «конечного автомата».

Создайте базовый класс State, который повторяет интерфейс класса Context. Каждый метод принимает один дополнительный параметр: экземпляр класса Context. Класс State может определять любое полезное поведение «по умолчанию».

Создайте производные от State классы для всех возможных состояний.

Все полученные от клиента запросы класс Context просто делегирует объекту «текущее состояние», при этом в качестве дополнительного параметра передается адрес объекта Context.

Используя этот адрес, в случае необходимости методы класса State могут изменить «текущее состояние» класса Context.

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

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

using namespace std;

class State *current; public:

void setCurrent(State *s)

void on(); void off();

virtual void on(Machine *m)

virtual void ofT(Machine *m)

void Machine::on()

current->on(this);

void Machine::off()

current->off(this);

class ON: public State

void off(Machine *m);

class OFF: public State

void on(Machine *m)

cout setCurrent(new ON()); delete this;

void ON::off(Machine *m)

cout setCurrent(new OFF()); delete this;

Machine::Machine()

current = new OFF(); cout

void(Machine:: *ptrs)() =

Machine::off, Machine::on

Machine fsm; int num; while (1)

(fsm. *ptrs}

Понравилась статья? Поделиться с друзьями: