ОС

Разница между паттернами Состояние и Стратегия в Java. Состояние Примеры в.NET Framework

Разница между паттернами Состояние и Стратегия в Java. Состояние Примеры в.NET Framework

В режиме государственного (государственного образца), поведение класса основывается на его статус изменился. Этот тип шаблонов проектирования относятся поведенческие модели.

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

введение

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

Главным образом решить: поведение объекта зависит от его состояния (атрибутов), и вы можете изменить его в соответствии с его государством, связанные с изменением поведения.

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

Как исправить: статус конкретных классов абстрактных вне.

Ключевой код: режим командного интерфейса, как правило, только один метод. Статус интерфейса, в котором один или несколько методов. Кроме того, метод государственного режима класса реализации, как правило, возвращаемого значения, или изменить значение переменной экземпляра. То есть, состояние и состояние объектной модели, как правило, актуальны. Методы класса реализации имеют различные функции, методы интерфейса охвачены. Режим Состояние и командный режим то же самое может быть использован для устранения других условий, если... Else выбор.

Примеры применения: 1, играть в баскетбол игрок может иметь нормальное состояние, а не нормальное состояние и ненормальное состояние. 2, маркиза Yi Цзэн колокола, то "часы абстрактный интерфейс", "часы А" и другие конкретные состояния, специфическая среда "" китайские колокола (контекст).

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

Недостатки: 1, Паттерн состояние использования связано с увеличением числа системных классов и объектов. 2, структура и реализация государственной формы являются более сложными, при неправильном использовании может вызвать путаницу структуру программы и код. 3, поддержка государственного образца "Открытый Закрытый принцип" не очень хорошо, вы можете переключать состояние государственной модели, добавляя новые классы нужно изменить статус лиц, ответственных за переходы состояний исходного кода, или не может перейти в новое состояние, и изменить состояние класс действовать также необходимо модифицировать исходный код соответствующего класса.

Сценарии использования: 1, с поведением изменения состояния и изменения сцены. 2, условный переход утверждение заменой.

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

реализация

Мы создадим интерфейс статуса и сущностигосударственный класс реализации интерфейсаState. Контекст представляет собой класс с определенным состоянием.

StatePatternDemo, мы демонстрируем использование объектовКонтекст Контекст класса и статуса, чтобы продемонстрировать изменение поведения в состоянии изменения.

Шаг 1

Создайте интерфейс.

State.java

Public interface State { public void doAction(Context context); }

Шаг 2

Создать класс сущностей, который реализует интерфейс.

StartState.java

Открытый класс StartState реализует государство { общественного недействительными DoAction (контекст Контекст) { System.out.println ("Игрок находится в стартовом состоянии"); context.setState (это); } общественного Строка ToString () { вернуть "начальное состояние"; } }

StopState.java

Public class StopState implements State { public void doAction(Context context) { System.out.println("Player is in stop state"); context.setState(this); } public String toString(){ return "Stop State"; } }

Шаг 3

Создание классаконтекста.

Context.java

Открытый класс {Context частное Государственное государство; общественный контекст () { состояние = NULL; } общественного недействительными SetState (Государственное государство) { this.state = состояние; } общенародного государства GetState () { возвращать состояние; } }

Шаг 4

Используйтеконтекст , чтобы увидеть поведение при изменении состояния измененийсостояния.

StatePatternDemo.java

Открытый класс StatePatternDemo { государственной статической силы основных (String ) {агдз Контекст Контекст = новый контекст (); StartState startState = новый StartState (); startState.doAction (контекст); System.out.println (context.getState () ToString ().); StopState stopState = новый StopState (); stopState.doAction (контекст); System.out.println (context.getState () ToString ().); } }

Шаг 5

Проверьте выход.

Игрок находится в стартовом состоянии Start государственный Игрок находится в состоянии останова остановленном

Состояние (англ. 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(). То есть формально вызывается один и тот же метод, но его поведение различно.

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

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

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

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

Сложность:

Популярность:

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

Примеры Состояния в стандартных библиотеках Java:

  • javax.faces.lifecycle.LifeCycle#execute() (контролируемый из FacesServlet : поведение зависит от текущей фазы (состояния) JSF)

Признаки применения паттерна: Методы класса делегируют работу одному вложенному объекту.

Аудиоплеер

Основной класс плеера меняет своё поведение в зависимости от того, в каком состоянии находится проигрывание.

states

states/State.java: Общий интерфейс состояний

package сайт.state.example..state.example.ui.Player; /** * Общий интерфейс всех состояний. */ public abstract class State { Player player; /** * Контекст передаёт себя в конструктор состояния, чтобы состояние могло * обращаться к его данным и методам в будущем, если потребуется. */ State(Player player) { this.player = player; } public abstract String onLock(); public abstract String onPlay(); public abstract String onNext(); public abstract String onPrevious(); }

states/LockedState.java: Состояние "заблокирован"

package сайт.state.example..state.example.ui.Player; /** * Конкретные состояния реализуют методы абстрактного состояния по-своему. */ public class LockedState extends State { LockedState(Player player) { super(player); player.setPlaying(false); } @Override public String onLock() { if (player.isPlaying()) { player.changeState(new ReadyState(player)); return "Stop playing"; } else { return "Locked..."; } } @Override public String onPlay() { player.changeState(new ReadyState(player)); return "Ready"; } @Override public String onNext() { return "Locked..."; } @Override public String onPrevious() { return "Locked..."; } }

states/ReadyState.java: Состояние "готов"

package сайт.state.example..state.example.ui.Player; /** * Они также могут переводить контекст в другие состояния. */ public class ReadyState extends State { public ReadyState(Player player) { super(player); } @Override public String onLock() { player.changeState(new LockedState(player)); return "Locked..."; } @Override public String onPlay() { String action = player.startPlayback(); player.changeState(new PlayingState(player)); return action; } @Override public String onNext() { return "Locked..."; } @Override public String onPrevious() { return "Locked..."; } }

states/PlayingState.java: Состояние "проигрывание"

package сайт.state.example..state.example.ui.Player; public class PlayingState extends State { PlayingState(Player player) { super(player); } @Override public String onLock() { player.changeState(new LockedState(player)); player.setCurrentTrackAfterStop(); return "Stop playing"; } @Override public String onPlay() { player.changeState(new ReadyState(player)); return "Paused..."; } @Override public String onNext() { return player.nextTrack(); } @Override public String onPrevious() { return player.previousTrack(); } }

ui

ui/Player.java: Проигрыватель

package сайт.state.example..state.example.states..state.example.states.State; import java.util.ArrayList; import java.util.List; public class Player { private State state; private boolean playing = false; private List playlist = new ArrayList<>(); private int currentTrack = 0; public Player() { this.state = new ReadyState(this); setPlaying(true); for (int i = 1; i <= 12; i++) { playlist.add("Track " + i); } } public void changeState(State state) { this.state = state; } public State getState() { return state; } public void setPlaying(boolean playing) { this.playing = playing; } public boolean isPlaying() { return playing; } public String startPlayback() { return "Playing " + playlist.get(currentTrack); } public String nextTrack() { currentTrack++; if (currentTrack > playlist.size() - 1) { currentTrack = 0; } return "Playing " + playlist.get(currentTrack); } public String previousTrack() { currentTrack--; if (currentTrack < 0) { currentTrack = playlist.size() - 1; } return "Playing " + playlist.get(currentTrack); } public void setCurrentTrackAfterStop() { this.currentTrack = 0; } }

ui/UI.java: GUI проигрывателя

package сайт.state.example.ui; import javax.swing.*; import java.awt.*; public class UI { private Player player; private static JTextField textField = new JTextField(); public UI(Player player) { this.player = player; } public void init() { JFrame frame = new JFrame("Test player"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel context = new JPanel(); context.setLayout(new BoxLayout(context, BoxLayout.Y_AXIS)); frame.getContentPane().add(context); JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER)); context.add(textField); context.add(buttons); // Контекст заставляет состояние реагировать на пользовательский ввод // вместо себя. Реакция может быть разной в зависимости от того, какое // состояние сейчас активно. JButton play = new JButton("Play"); play.addActionListener(e -> textField.setText(player.getState().onPlay())); JButton stop = new JButton("Stop"); stop.addActionListener(e -> textField.setText(player.getState().onLock())); JButton next = new JButton("Next"); next.addActionListener(e -> textField.setText(player.getState().onNext())); JButton prev = new JButton("Prev"); prev.addActionListener(e -> textField.setText(player.getState().onPrevious())); frame.setVisible(true); frame.setSize(300, 100); buttons.add(play); buttons.add(stop); buttons.add(next); buttons.add(prev); } }

Demo.java: Клиентский код

package refactoring_guru.state..state.example.ui..state.example.ui.UI; /** * Демо-класс. Здесь всё сводится воедино. */ public class Demo { public static void main(String args) { Player player = new Player(); UI ui = new UI(player); ui.init(); } }

Пришло время исповедаться: я немного перестарался с этой главной. Предполагалось, что она посвящена шаблону проектирования Состояние (State) GoF . Но я не могу говорить о его применении в играх, не затрагивая концепцию конечных автоматов (finite state machines) (или "FSM"). Но как только я в нее углубился, я понял, что мне придется вспомнить иерархическую машину состояний (hierarchical state machine) или иерархический автомат и автомат с магазинной памятью (pushdown automata) .

Тематика получается слишком обширной, поэтому чтобы сократить главу до минимума, я буду опускать некоторые очевидные фрагменты примеров кода и вам придется заполнить некоторые пропуски самостоятельно. Я надеюсь, это не сделает их менее понятными.

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

Все это отголоски старых ранних деньков искусственного интеллекта. В 50-е и 60-е искусственный интеллект в основном фокусировался на обработке языковых конструкций. Многие используемые в современных компиляторах технологии были изобретены для парсинга человеческих языков.

Все мы там были

Допустим мы работаем над небольшим платформером сайд-скроллером. Наша задача заключается в моделировании героини, которая будет аватаром игрока в игровом мире. Это значит, что она должна реагировать на пользовательский ввод. Нажмите B и она прыгнет. Довольно просто:

void Heroine::handleInput(Input input) { if (input == PRESS_B) { yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); } }

Заметили баг?

Здесь нет никакого кода, предотвращающего "прыжок в воздухе"; продолжайте нажимать B пока она в воздухе и она будет подлетать снова и снова. Проще всего решить это добавлением булевского флага isJumping_ в Heroine , который будет следить за тем когда героиня прыгнула:

void Heroine::handleInput(Input input) { if (input == PRESS_B) { if (!isJumping_) { isJumping_ = true ; // Прыжок... } } }

Нам нужен еще и код, который будет устанавливать isJumping_ обратно в false , когда героиня снова коснется земли. Для простоты я опускаю этот код.

void Heroine::handleInput(Input input) { if (input == PRESS_B) { // Прыгаем если уже не прыгнули... } else if (input == PRESS_DOWN) { if (!isJumping_) { setGraphics(IMAGE_DUCK); } } else if (input == RELEASE_DOWN) { setGraphics(IMAGE_STAND); } }

А здесь баг заметили?

С помощью этого кода игрок может:

  1. Нажать вниз для приседания.
  2. Нажать B для прыжка из сидячей позиции.
  3. Отпустить вниз, находясь в воздухе.

При этом героиня переключится на графику стояния прямо в воздухе. Придется добавить еще один флаг...

void Heroine::handleInput(Input input) { if (input == PRESS_B) { if (!isJumping_ && !isDucking_) { // Прыжок... } } else if (input == PRESS_DOWN) { if (!isJumping_) { isDucking_ = true ; setGraphics(IMAGE_DUCK); } } else if (input == RELEASE_DOWN) { if (isDucking_) { isDucking_ = false ; setGraphics(IMAGE_STAND); } } }

Теперь будет здорово добавить героине способность атаковать подкатом, когда игрок нажимает вниз, находясь в воздухе:

void Heroine::handleInput(Input input) { if (input == PRESS_B) { if (!isJumping_ && !isDucking_) { // Прыжок... } } else if (input == PRESS_DOWN) { if (!isJumping_) { isDucking_ = true ; setGraphics(IMAGE_DUCK); } else { isJumping_ = false ; setGraphics(IMAGE_DIVE); } } else if (input == RELEASE_DOWN) { if (isDucking_) { // Стояние... } } }

Снова ищем баги. Нашли?

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

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

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

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

Конечные автоматы — наше спасение

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

Поздравляю, вы только что создали конечный автомат (finite state machine ). Они пришли из области компьютерных наук, называемой теория автоматов (automata theory ), в семейство структур которой также входит знаменитая машина Тьюринга. FSM - простейший член этого семейства.

Суть заключается в следующем:

    У нас есть фиксированный набор состояний , в которых может находиться автомат. В нашем примере это стояние, прыжок, приседание и подкат.

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

    Последовательность ввода или событий , передаваемых автомату. В нашем примере это нажатие и отпускание кнопок.

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

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

В чистой форме это и есть целый банан: состояния, ввод и переходы. Можно изобразить их в виде блок-схемы. К сожалению, компилятор таких каракулей не поймет. Так как же в таком случае реализовать конечный автомат? Банда Четырех предлагает свой вариант, но начнем мы с еще более простого.

Моя любимая аналогия FSM — это старый текстовый квест Zork . У вас есть мир, состоящий из комнат, которые соединены между собой переходами. И вы можете исследовать их, вводя команды типа "идти на север".

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

Перечисления и переключатели

Одна из проблем нашего старого класса Heroine заключается в том, что он допускает некорректную комбинацию булевских ключей: isJumping_ и isDucking_ , они не могут быть истинными одновременно. А если у вас есть несколько булевских флагов, только один из которых может быть true , не лучше ли заменить их все на enum .

В нашем случае с помощью enum можно полностью описать все состояния нашей FSM таким образом:

enum State { STATE_STANDING, STATE_JUMPING, STATE_DUCKING, STATE_DIVING };

Вместо кучи флагов, у Heroine есть только одно поле state_ . Также нам придется изменить порядок ветвления. В предыдущем примере кода, мы делали ветвление сначала в зависимости от ввода, а потом уже от состояния. При этом мы группировали код по нажатой кнопке, но размывали код, связанный с состояниями. Теперь мы сделаем наоборот и будем переключать ввод в зависимости от состояния. Получим мы вот что:

void Heroine::handleInput(Input input) { switch (state_) { case STATE_STANDING: if (input == PRESS_B) { state_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); } else if (input == PRESS_DOWN) { state_ = STATE_DUCKING; setGraphics(IMAGE_DUCK); } break ; case STATE_JUMPING: if (input == PRESS_DOWN) { state_ = STATE_DIVING; setGraphics(IMAGE_DIVE); } break ; case STATE_DUCKING: if (input == RELEASE_DOWN) { state_ = STATE_STANDING; setGraphics(IMAGE_STAND); } break ; } }

Выглядит довольно тривиально, но тем не менее этот код уже гораздо лучше, чем предыдущий. У нас остались некоторые условные ветвления, но зато мы упростили изменяемое состояние до единственного поля. Весь код, управляющий единственным состоянием собран в одном месте. Это самый простой способ реализации конечного автомата и иногда его вполне достаточно.

Теперь героиня уже не сможет быть в неопределенном состоянии. При использовании булевых флагов некоторые комбинации были возможны, но не имели смысла. При использовании enum все значения корректны.

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

Добавляем в Heroine поле chargeTime_ для хранения времени зарядки. Допустим у нас уже есть метод update() , вызываемый на каждом кадре. Добавим в него следующий код:

void Heroine::update() { if (state_ == STATE_DUCKING) { chargeTime_++; if (chargeTime_ > MAX_CHARGE) { superBomb(); } } }

Если вы угадали, что это шаблон Метод обновления (Update Method) , вы выиграли приз!

Каждый раз, когда мы приседаем заново, нам нужно обнулять этот таймер. Для этого нам нужно изменить handleInput() :

void Heroine::handleInput(Input input) { switch (state_) { case STATE_STANDING: if (input == PRESS_DOWN) { state_ = STATE_DUCKING; chargeTime_ = 0 ; setGraphics(IMAGE_DUCK); } // Обработка оставшегося ввода... break ; // Другие состояния... } }

В конце концов, для добавления этой атаки с подзарядкой, нам пришлось изменить два метода и добавить поле chargeTime_ в Heroine , даже если оно используется только в состоянии приседания. Хотелось бы иметь весь этот код и данные в одном месте. Банда Четырех может нам в этом помочь.

Шаблон состояние

Для людей, хорошо разбирающихся в объектно-ориентированной парадигме, каждое условное ветвление — это возможность для использования динамической диспетчеризации (другими словами, вызова виртуального метода в C++). Думаю нам нужно спуститься в эту кроличью нору еще глубже. Иногда if — это все что нам нужно.

Этому есть историческое обоснование. Многие из старых апостолов объектно-ориентированной парадигмы, такие как Банда Четырех со своими Паттернами программирования и Мартин Фулер с его Рефакторингом пришли из Smalltalk . А там ifThen — это всего лишь метод, которым вы обрабатываете условие и который реализуется по разному для объектов true и false .

В нашем примере мы уже добрались до той критической точки, когда нам стоит обратить внимание на что-то объектно-ориентированное. Это подводит нас к шаблону Состояние. Цитирую Банду Четырех:

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

Не очень то и понятно. В конце концов и switch с этим справляется. Применительно к нашему примеру с героиней шаблон будет выглядеть следующим образом:

Интерфейс состояния

Для начала определим интерфейс для состояния. Каждый бит поведения, зависящий от состояния — т.е. все что мы раньше реализовывали при помощи switch — превращается в виртуальный метод этого интерфейса. В нашем случае это handleInput() и update() .

class HeroineState { public : virtual ~HeroineState() {} virtual void handleInput {} {} };

Классы для каждого из состояний

Для каждого состояния мы определяем класс, реализующий интерфейс. Его методы определяют поведение героини в данном состоянии. Другими словами берем все варианты из switch в предыдущем примере превращаем их в класс состояния. Например:

class DuckingState: public HeroineState { public : DuckingState() : chargeTime_(0 ) {} virtual void handleInput (Heroine& heroine, Input input) { if (input == RELEASE_DOWN) { // Переход в состояние стояния... heroine.setGraphics(IMAGE_STAND); } } virtual void update (Heroine& heroine) { chargeTime_++; if (chargeTime_ > MAX_CHARGE) { heroine.superBomb(); } } private : int chargeTime_; };

Обратите внимание, что мы перенесли chargeTime_ из класса самой героини в класс DuckingState . И это очень хорошо, потому что этот кусок данных имеет значение только в этом состоянии и наша модель данных явно об этом свидетельствует.

Делегирование к состоянию

class Heroine { public : virtual void handleInput (Input input) { state_->handleInput(*this , input); } virtual void update () { state_->update(*this ); } // Другие методы... private : HeroineState* state_; };

Чтобы "изменить состояние" нам нужно просто сделать так, чтобы state_ указывал на другой объект HeroineState . В этом собственно и заключается шаблон Состояние.

Выглядит довольно похоже на шаблоны Стратегия (Strategy) GoF и Объект тип (Type Object) . Во всех трёх у нас есть главный объект, делегирующий к подчиненному. Различие в назначении .

  • Цель Стратегии заключается в уменьшении связности (decouple) между главным классом и его поведением.
  • Целью Объект тип (Type Object) является создание некоторого количества объектов, ведущих себя одинаково с помощью разделения между собой общего объекта типа.
  • Целью Состояния является изменение поведения главного объекта через изменение объекта к которому он делегирует.

А где же эти объекты состояния?

Я вам кое-что не сказал. Чтобы изменить состояние, нам нужно присвоить state_ новое значение, указывающее на новое состояние, но откуда этот объект возьмется? В нашем примере с enum думать не о чем: значения enum — это просто примитивы наподобие чисел. Но теперь наши состояния представлены классами и это значит, что нам нужны указатели на реальные экземпляры. Существует два самых распространенных ответа:

Статические состояния

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

Если у вашего состояния нет полей и только один виртуальный метод, можно еще сильнее упростить шаблон. Заменим каждый класс состояния функцией состояния — обычной функцией верхнего уровня. И соответственно поле state_ в нашем главном классе превратится в простой указатель на функцию.

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

Куда вы поместите статический экземпляр — это уже ваше дело. Найдите такое место, где это будет уместно. Давайте поместим наш экземпляр в базовый класс. Без всякой причины.

class HeroineState { public : static StandingState standing; static DuckingState ducking; static JumpingState jumping; static DivingState diving; // Остальной код... };

Каждое из этих статических полей — экземпляр состояния, используемого игрой. Чтобы заставить героиню подпрыгнуть, состояние стояния сделает нечто вроде:

if (input == PRESS_B) { heroine.state_ = &HeroineState::jumping; heroine.setGraphics(IMAGE_JUMP); }

Экземпляры состояний

Иногда предыдущий вариант не взлетает. Статическое состояние не подойдет для состояния присядки. У него есть поле chargeTime_ и оно специфично для героини, которая будет приседать. Это еще худо бедно сработает в нашем случае, потому что у нас всего одна героиня, но если мы захотим добавить кооператив для двух игроков, у нас будут большие проблемы.

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

Вместо этого, мы позволим handleInput() в HeroineState опционально возвращать новое состояние. Когда это произойдет, Heroine удалит старое состояние и поменяет его на новое, например, так:

void Heroine::handleInput(Input input) { HeroineState* state = state_->handleInput(*this , input); if (state != NULL ) { delete state_; state_ = state; } }

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

HeroineState* StandingState::handleInput(Heroine& heroine, Input input) { if (input == PRESS_DOWN) { // Other code... return new DuckingState(); } // Stay in this state. return NULL ; }

Когда у меня получается, я предпочитаю использовать статические состояния, потом что они не занимают память и такты процессора, выделяя объекты при каждом изменении состояния. Для состояний, которые не представляют из себя нечто большее, чем просто состояния — это как раз то что нужно.

Конечно, когда вы выделяете память под состояние динамически, вам стоит подумать о возможной фрагментации памяти. Помочь может шаблон Пул объектов (Object Pool) .

Действия для входа и выхода

Шаблон Состояние предназначен для инкапсуляции всего поведения и связанных с ним данных внутри одного класса. У нас довольно неплохо получается, но остались некоторые невыясненные детали.

Когда героиня изменяет состояние, мы также переключаем ее спрайт. Прямо сейчас, этот код принадлежит состоянию, с которого она переключается. Когда состояние переходит от нырка в состояние стояния, то нырок устанавливает ее образ:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input) { if (input == RELEASE_DOWN) { heroine.setGraphics(IMAGE_STAND); return new StandingState(); } // Other code... }

То, что мы действительно хотим, каждое состояние контролировало свою собственную графику. Мы можем добиться этого, добавив в состояние входное действие (entry action ):

class StandingState: public HeroineState { public : virtual void enter (Heroine& heroine) { heroine.setGraphics(IMAGE_STAND); } // Other code... };

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

void Heroine::handleInput(Input input) { HeroineState* state = state_->handleInput(*this , input); if (state != NULL ) { delete state_; state_ = state; // Вызов входного действия нового состояния. state_->enter(*this ); } }

Это позволит упростить код состояния DuckingState:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input) { if (input == RELEASE_DOWN) { return new StandingState(); } // Other code... }

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

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

Можно по аналогии сделать и выходное действие (exit action ). Это будет просто метод, который мы будем вызывать для состояния, перед тем, как покидаем его и переключаемся на новое состояние.

И чего же мы добились?

Я столько времени потратил, чтобы продать вам FSM , а теперь собираюсь вырвать коврик из под ног. Все, что я до сих пор говорил — правда и отлично решает проблемы. Но, так уж вышло, что самые главные достоинства конечных автоматов, одновременно являются и их самыми большими недостатками.

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

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

"Полнота по Тьюригну" означает систему (обычно язык программирования), обладающую достаточной выразительностью для реализации машины Тьюринга. В свою очередь это означает что все полные по Тьюрингу языки примерно одинаково выразительны. FSM недостаточно выразительны чтобы войти в этот клуб.

Если же вы попробуете использовать машину состояний для чего-либо более сложного, как например игровой AI , вы сразу уткнетесь в ограничения этой модели. К счастью, наши предшественники научились обходить некоторые препятствия. Я закончу эту главу несколькими такими примерами.

Машина конкурентных состояний

Мы решили добавить нашей героине возможность носить оружие. Хотя она теперь вооружена, она по прежнему может делать все, что делала раньше: бегать, прыгать, приседать и т.д. Но теперь, делая все это, она еще может и стрелять из оружия.

Если мы захотим вместить такое поведение в рамки FSM , нам придется удвоить количество состояний. Для каждого из состояний нам придется завести еще одно такое же, но уже для героини с оружием: стояние, стояние с оружием, прыжок, прыжок с оружием.... Ну вы поняли.

Если добавить еще несколько видов оружия, количество состояний увеличится комбинаторно. И это не просто куча состояний, а еще и куча повторов: вооруженное и безоружное состояния практически идентичны за исключением части кода, отвечающей за стрельбу.

Проблема здесь в том, что мы смешиваем две части состояния — что она делает и что держит в руках — в один автомат. Чтобы смоделировать все возможные комбинации, нам нужно завести состояние для каждой пары . Решение очевидно: нужно завести два отдельных конечных автомата.

Если мы хотим объединить n состояний действия и m состояний того, что держим в руках в один конечный автомат — нам нужно n × m состояний. Если у нас будет два автомата — нам понадобится n + m состояний.

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

class Heroine { // Остальной код... private : HeroineState* state_; HeroineState* equipment_; };

Для иллюстрации мы используем полную реализацию шаблона Состояние для второго конечного автомата, хотя на практике в данном случае хватило бы простого булевского флага.

Когда героиня делегирует ввод состояниям, она передает перевод обеим конечным автоматам:

void Heroine::handleInput(Input input) { state_->handleInput(*this , input); equipment_->handleInput(*this , input); }

Более сложные системы могут иметь в своем составе конечные автоматы, которые могут поглощать часть ввода таким образом чтобы другие автоматы его уже не получали. Это позволит нам предотвратить ситуацию, когда несколько автоматов реагируют на один и тот же ввод.

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

На практике вы можете встретить ситуацию, когда состояния взаимодействуют друг с другом. Например, она не может выстрелить в прыжке или например выполнить атаку с подкатом когда вооружена. Чтобы обеспечить такое поведение и координацию автоматов в коде, вам придется вернуться к той же самой грубой проверке через if другого конечного автомата. Не самое элегантное решение, но по крайней мере работает.

Иерархическая машина состояний

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

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

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

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

В таком виде получившаяся структура будет называться иерархическая машина состояний (или иерархический автомат ). А у каждого состояния может быть свое суперсостояние (само состояние при этом называется подсостоянием ). Когда наступает событие и подсостояние его не обрабатывает, оно передается по цепочке суперсостояний вверх. Другими словами, получается подобие переопределения унаследованного метода.

На самом деле, если мы используем оригинальный шаблон Состояние для реализации FSM , мы уже можем использовать наследование классов для реализации иерархии. Определим базовый класс для суперкласса:

class OnGroundState: public HeroineState { public : virtual void handleInput (Heroine& heroine, Input input) { if (input == PRESS_B) { // Подпрыгнуть... } else if (input == PRESS_DOWN) { // Присесть... } } };

А теперь каждый подкласс будет его наследовать:

class DuckingState: public OnGroundState { public : virtual void handleInput (Heroine& heroine, Input input) { if (input == RELEASE_DOWN) { // Встаем... } else { // Ввод не обработан. Поэтому передаем его выше по иерархии. OnGroundState::handleInput(heroine, input); } } };

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

Текущее состояние будет находится вверху стека, под ним его суперсостояние, дальше суперсостояние для этого суперсостояния и т.д. И когда вам нужно будет реализовать специфичное для состояния поведение, вы начнете с верха стека спускаться по нему вниз, пока состояние его не обработает. (А если не обработает — значит вы его просто игнорируете).

Автомат с магазинной памятью

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

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

Вот простой пример: Ранее мы позволили нашей бесстрашной героине вооружиться до зубов. Когда она стреляет из своего оружия, нам нужно новое состояние для проигрывания анимации выстрела, порождения пули и сопутствующих визуальных эффектов. Для этого мы создаем новое FiringState и делаем в него переходы из всех состояний, в которых героиня может стрелять по нажатию кнопки стрельбы.

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

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

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

Что нам на самом деле нужно — так это возможность хранить состояние, в котором мы находились до стрельбы и после стрельбы вспоминать его снова. Здесь нам снова может помочь теория автоматов. Соответствующая структура данных называется Автомат с магазинной памятью (Pushdown Automaton) .

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

    Вы можете поместить (push ) новое состояние в стек. Текущее состояние всегда будет находиться вверху стека, так что это и есть операция перехода в новое состояние. Но при этом старое состояние остается прямо под текущим в стеке, а не исчезает бесследно.

    Вы можете извлечь (pop ) верхнее состояние из стека. Состояние пропадает и текущим становится то что находилось под ним.

Это все что нам нужно для стрельбы. Мы создаем единственное состояние стрельбы. Когда мы нажимаем кнопку стрельбы, находясь в другом состоянии, мы помещаем (push ) состояние стрельбы в стек. Когда анимация стрельбы заканчивается, мы извлекаем (pop ) состояние и автомат с магазинной памятью автоматически возвращает нас в предыдущее состояние.

Насколько они реально полезны?

Даже с этим расширением конечных автоматов, их возможности все равно довольно ограничены. В AI сегодня преобладает тренд использования вещей типа деревьев поведения (behavior trees) и систем планирования (planning systems). И если вам интересна именно область AI , вся эта глава должна просто раздразнить ваш аппетит. Чтобы его удовлетворить, вам придется обратиться к другим книгам.

Это совсем не значит, что конечные автоматы, автоматы с магазинной памятью и другие подобные системы полностью бесполезны. Для некоторых вещей это хорошие инструменты для моделирования. Конечные автоматы полезны когда:

  • У вас есть сущность, поведение которой изменяется в зависимости от ее внутреннего состояния.
  • Это состояние жестко делится на относительно небольшое количество конкретных вариантов.
  • Сущность постоянно отвечает на серии команд ввода или событий.

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

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

Проблема

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


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

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

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

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

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

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

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

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

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