Домой / Облачные хранилища / Состояние state gof пример на с. Разница между паттернами Состояние и Стратегия в Java. Условия, Задача, Назначение

Состояние state gof пример на с. Разница между паттернами Состояние и Стратегия в Java. Условия, Задача, Назначение

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

Паттерн «Состояние» предполагает выделение базового класса или интерфейса для всех допустимых операций и наследника для каждого возможного состояния

Когда использовать Паттерн State

    Когда поведение объекта должно зависеть от его состояния и может изменяться динамически во время выполнения

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

UML схема паттерна "Состояние":

Реализация шаблона "состояние" на C#

using System; namespace DoFactory.GangOfFour.State.Structural { /// /// MainApp startup class for Structural /// State Design Pattern. /// class MainApp { /// /// Entry point into console application. /// static void Main() { // Setup context in a state Context c = new Context(new ConcreteStateA()); // Issue requests, which toggles state c.Request(); c.Request(); c.Request(); c.Request(); // Wait for user Console.ReadKey(); } } /// /// The "State" abstract class /// abstract class State { public abstract void Handle(Context context); } /// class ConcreteStateA: State { public override void Handle(Context context) { context.State = new ConcreteStateB(); } } /// /// A "ConcreteState" class /// class ConcreteStateB: State { public override void Handle(Context context) { context.State = new ConcreteStateA(); } } /// /// The "Context" class /// class Context { private State _state; // Constructor public Context(State state) { this.State = state; } // Gets or sets the state public State State { get { return _state; } set { _state = value; Console.WriteLine("State: " + _state.GetType().Name); } } public void Request() { _state.Handle(this); } } }

Пример паттерна State из реальной жизни

Примеры в.NET Framework

  • CommunicationObject реализует конечный автомат перехода между состояниями WCF клиента: Created, Opening, Opened, Closing, Closed и Faulted.
  • Task реализует конечный автомат перехода между состояниями задачи: Created, WaitingForActivation, WaitingToRun, Running, Run ToCompletion, Canceled, Faulted.

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

Проблема

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


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

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

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

Машину состояний чаще всего реализуют с помощью множества условных операторов, 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#).

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

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

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

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

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

using System; namespace State { ///

/// Заблокированное состояние счета. /// public class Blocked: IState { /// /// Пополнить счет. /// /// Пополняемый счет. /// Сумма пополнения. public void Deposit(Card card, decimal money) { // Проверяем входные аргументы на корректность. if (card == null) { throw new ArgumentNullException(nameof(card)); } if (money <= 0) { throw new ArgumentException("Вносимая сумма должна быть больше нуля.", nameof(money)); } // Вычисляем сумму сверхлимитной задолженности. var overdraft = card.CreditLimit - card.Credit; // Вычисляем насколько сумма пополнения перекрывает задолженность. var difference = money - overdraft; if (difference < 0) { // Если сумма пополнения не перекрывает задолженность, // то просто уменьшаем сумму задолженности. card.Credit += money; // Вычисляем процент оставшейся суммы на счете. var limit = card.Credit / card.CreditLimit * 100; if (limit < 10) { // Если после пополнения на счете все еще меньше десяти процентов от лимита, // то просто сообщаем об этом пользователю. Console.WriteLine($"Ваш счет пополнен на сумму {money}. " + $"Сумма на вашем счете все еще составляет менее 10%. Ваш счет остался заблокирован. Пополните счет на большую сумму. {card.ToString()}"); } else if (limit >= 10 && limit < 100) { // Если задолженность перекрыта не полностью, то переводим в состояние расходования кредитных средств. card.State = new UsingCreditFunds(); Console.WriteLine($"Ваш счет пополнен на сумму {money}. Задолженность частично погашена. " + $"Погасите задолженность в размере {Math.Abs(difference)} рублей. {card.ToString()}"); } else { // Иначе задолженность полностью погашена, переводим в состояние расходования собственных средств. card.State = new UsingOwnFunds(); Console.WriteLine($"Ваш счет пополнен на {money} рублей. Задолженность полностью погашена. {card.ToString()}"); } } else { // Иначе закрываем задолженность, а оставшиеся средства переводим в собственные средства. card.Credit = card.CreditLimit; card.Debit = difference; // Переводим карту в состояние использования собственных средств. card.State = new UsingOwnFunds(); Console.WriteLine($"Ваш счет пополнен на {money} рублей. " + $"Кредитная задолженность погашена. {card.ToString()}"); } } /// /// Расходование со счета. /// /// Счет списания. /// Стоимость покупки. /// Успешность выполнения операции. public bool Spend(Card card, decimal price) { // Отказываем в операции. Console.WriteLine($"Ваш счет заблокирован. Пополните счет. {card.ToString()}"); return false; } } }

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

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

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

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

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

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

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

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

    Вводит класс Context, в котором определяется интерфейс для внешнего мира.

    Вводит абстрактный класс State.

    Представляет различные "состояния" конечного автомата в виде подклассов State.

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

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

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

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

UML-диаграмма классов паттерна State

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

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

    Выдать покупателю товар, выдавать сдачу не требуется.

    Выдать покупателю товар и сдачу.

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

    Покупатель товар не получит из-за его отсутствия.

Реализация

Рисунок 1. Диаграмма классов паттерна Состояние.

Паттерн состоит из 3 блоков:

    Widget – класс, объекты которого должны менять свое поведение в зависимости от состояния.

    IState – интерфейс, который должен реализовать каждое из конкретных состояний. Через этот интерфейс объект Widget взаимодействует с состоянием, делегируя ему вызовы методов. Интерфейс должен содержать средства для обратной связи с объектом, поведение которого нужно изменить. Для этого используется событие (паттерн Publisher - Subscriber). Это необходимо для того, чтобы в процессе выполнения программы заменять объект состояния при появлении событий. Возможны случаи, когда сам Widget периодически опрашивает объект состояние на наличие перехода.

    StateA … StateZ – классы конкретных состояний. Должны содержать информацию о том, при каких условиях и в какие состояния может переходить объект из текущего состояния. Например, из StateA объект может переходить в состояние StateB и StateC, а из StateB – обратно в StateA и так далее. Объект одного из них должен содержать Widget при создании.

public interface IState

event StateHandler NextState;

void SomeMethod();

public interface IWidget

void SomeMethod();

public class StateA: IState

public void SomeMethod()

Console.WriteLine("StateA.SomeMethod");

if (NextState != null)

NextState(new StateB());

public class StateB: IState

public event StateHandler NextState;

public void SomeMethod()

Console.WriteLine("StateB.SomeMethod");

if (NextState != null)

NextState(new StateA());

public delegate void StateHandler(IState state);

public class Widget: IWidget

public Widget(IState state)

OnNextState(state);

private void OnNextState(IState state)

if (state == null)

throw new ArgumentNullException("state");

if (State != state)

State.NextState += new StateHandler(OnNextState);

private IState state;

public void SomeMethod()

state.SomeMethod();

public IState State

get { return state; }

set { state = value; }

Рассмотрим пример:

IWidget widget = new Widget(new StateA());

widget.SomeMethod();

widget.SomeMethod();

При создании объекта Widget через параметр конструктора передается объект, инкапсулирующий состояние. Это состояние будет являться текущим, на его событие NextState подписывается метод OnNextState(), который заменяет state на присланный объект состояния. При вызове метода SomeMethod() в первый раз объект Widget делегирует этот вызов объекту StateA. После того, как метод StateA.SomeMethod() выполнился, объект вызовет событие NextState и передаст в параметр объект StateB, который заменяет текущее состояние StateA. При вызове SomeMethod() второй раз будет вызван StateB.SomeMethod(). То есть формально вызывается один и тот же метод, но его поведение различно.

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

Назначение паттерна 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)(); } }