Состояние state gof пример на с. Паттерн Состояние (State pattern)

15.02.2016
21:30

Паттерн Состояние (State) предназначен для проектирования классов, которые имеют несколько независимых логических состояний. Давайте сразу перейдем к рассмотрению примера.

Допустим, мы разрабатываем класс управления веб-камерой. Камера может находиться в трех Состояниях:

  1. Не инициализирована. Назовем NotConnectedState ;
  2. Инициализирована и готова к работе, но кадры еще не захватываются. Пусть это будет ReadyState ;
  3. Активный режим захвата кадров. Обозначим ActiveState .

Поскольку мы работаем с паттером Состояние, то лучше всего начать с изображения Диаграммы состояний:

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

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

#include #define DECLARE_GET_INSTANCE(ClassName) \ static ClassName* getInstance() {\ static ClassName instance;\ return &instance;\ } class WebCamera { public: typedef std::string Frame; public: // ************************************************** // Exceptions // ************************************************** class NotSupported: public std::exception { }; public: // ************************************************** // States // ************************************************** class NotConnectedState; class ReadyState; class ActiveState; class State { public: virtual ~State() { } virtual void connect(WebCamera*) { throw NotSupported(); } virtual void disconnect(WebCamera* cam) { std::cout << "Деинициализируем камеру..." << std::endl; // ... cam->changeState(NotConnectedState::getInstance()); } virtual void start(WebCamera*) { throw NotSupported(); } virtual void stop(WebCamera*) { throw NotSupported(); } virtual Frame getFrame(WebCamera*) { throw NotSupported(); } protected: State() { } }; // ************************************************** class NotConnectedState: public State { public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) { std::cout << "Инициализируем камеру..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); } void disconnect(WebCamera*) { throw NotSupported(); } private: NotConnectedState() { } }; // ************************************************** class ReadyState: public State { public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) { std::cout << "Запускаем видео-поток..." << std::endl; // ... cam->changeState(ActiveState::getInstance()); } private: ReadyState() { } }; // ************************************************** class ActiveState: public State { public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) { std::cout << "Останавливаем видео-поток..." << std::endl; // ... cam-> << "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } }; public: explicit WebCamera(int camID) : m_camID(camID), m_state(NotConnectedState::getInstance()) { } ~WebCamera() { try { disconnect(); } catch(const NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } } void connect() { m_state->connect(this); } void disconnect() { m_state->disconnect(this); } void start() { m_state->start(this); } void stop() { m_state->stop(this); } Frame getFrame() { return m_state->getFrame(this); } private: void changeState(State* newState) { m_state = newState; } private: int m_camID; State* m_state; };

Обращаю внимание на макрос DECLARE_GET_INSTANCE . Конечно, использование макросов в C++ не поощряется. Однако это относится к случаям, когда макрос выступает в роли аналога шаблонной функции. В этом случае всегда отдавайте предпочтение последним.

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

Классы-Состояния мы объявляем в главном классе - WebCamera . Для краткости я использовал inline -определения функций-членов всех классов. Однако в реальных приложениях лучше следовать рекомендациям о разделении объявления и реализации по h и cpp файлам.

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

Основу иерархии классов состояний образует абстрактный класс WebCamera::State:

Class State { public: virtual ~State() { } virtual void connect(WebCamera*) { throw NotSupported(); } virtual void disconnect(WebCamera* cam) { std::cout << "Деинициализируем камеру..." << std::endl; // ... cam->changeState(NotConnectedState::getInstance()); } virtual void start(WebCamera*) { throw NotSupported(); } virtual void stop(WebCamera*) { throw NotSupported(); } virtual Frame getFrame(WebCamera*) { throw NotSupported(); } protected: State() { } };

Все его функции-члены соответствуют функциям самого класса WebCamera . Происходит непосредственное делегирование:

Class WebCamera { // ... void connect() { m_state->connect(this); } void disconnect() { m_state->disconnect(this); } void start() { m_state->start(this); } void stop() { m_state->stop(this); } Frame getFrame() { return m_state->getFrame(this); } // ... State* m_state; }

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

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

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

При этом для WebCamera::State::disconnect() мы предусматриваем реализацию по умолчанию. Такое поведение подойдет для двух состояний из трех. В результате мы предотвращаем дублирование кода.

Для смены состояния предназначена закрытая функция-член WebCamera::changeState() :

Void changeState(State* newState) { m_state = newState; }

Теперь к реализации конкретных Состояний. Для WebCamera::NotConnectedState достаточно переопределить операции connect() и disconnect() :

Class NotConnectedState: public State { public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) { std::cout << "Инициализируем камеру..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); } void disconnect(WebCamera*) { throw NotSupported(); } private: NotConnectedState() { } };

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

Другим важным элементом представленной реализации является то, что в новое Состояние мы переходим лишь в случае успеха. Например, если во время инициализации камеры произойдет сбой, то в Состояние ReadyState переходить рано. Главная мысль - полное соответствие фактического состояния камеры (в нашем случае) и объекта-Состояния.

Итак, камера готова к работе. Заведем соответствующий класс Состояния WebCamera::ReadyState:

Class ReadyState: public State { public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) { std::cout << "Запускаем видео-поток..." << std::endl; // ... cam->changeState(ActiveState::getInstance()); } private: ReadyState() { } };

Из Состояния готовности мы можем попасть в активное Состояние захвата кадров. Для этого предусмотрена операция start() , которую мы и реализовали.

Наконец мы дошли до последнего логического Состояния работы камеры WebCamera::ActiveState:

Class ActiveState: public State { public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) { std::cout << "Останавливаем видео-поток..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); } Frame getFrame(WebCamera*) { std::cout << "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } };

В этом Состоянии можно прервать захват кадров с помощью stop() . В результате мы попадем обратно в Состояние WebCamera::ReadyState . Кроме того, мы можем получать кадры, которые накапливаются в буфере камеры. Для простоты под "кадром" мы понимаем обычную строку. В реальности это будет некоторый байтовый массив.

А теперь мы можем записать типичный пример работы с нашим классом WebCamera:

Int main() { WebCamera cam(0); try { // cam в Состоянии NotConnectedState cam.connect(); // cam в Состоянии ReadyState cam.start(); // cam в Состоянии ActiveState std::cout << cam.getFrame() << std::endl; cam.stop(); // Можно было сразу вызвать disconnect() // cam в Состоянии ReadyState cam.disconnect(); // cam в Состоянии NotConnectedState } catch(const WebCamera::NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } return 0; }

Вот что в результате будет выведено на консоль:

Инициализируем камеру... Запускаем видео-поток... Получаем текущий кадр... Current frame Останавливаем видео-поток... Деинициализируем камеру...

А теперь попробуем спровоцировать ошибку. Вызовем connect() два раза подряд:

Int main() { WebCamera cam(0); try { // cam в Состоянии NotConnectedState cam.connect(); // cam в Состоянии ReadyState // Но для этого Состояния операция connect() не предусмотрена! cam.connect(); // Выбрасывает исключение NotSupported } catch(const WebCamera::NotSupported& e) { std::cout << "Произошло исключение!!!" << std::endl; // ... } catch(...) { // Обрабатываем исключение } return 0; }

Вот что из этого получится:

Инициализируем камеру... Произошло исключение!!! Деинициализируем камеру...

Обратите внимание, что камера все же была деинициализирована. Вызов disconnect() произошел в деструкторе WebCamera . Т.е. внутреннее Состояние объекта осталось абсолютно корректным.

Выводы

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

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

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

Особенности паттерна на 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(); } }

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

Проблема

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


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

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

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

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

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

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

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

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

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

Последнее обновление: 31.10.2015

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

Когда применяется данный паттерн?

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

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

UML-диаграмма данного шаблона проектирования предлагает следующую систему:

Формальное определение паттерна на C#:

Class Program { static void Main() { Context context = new Context(new StateA()); context.Request(); // Переход в состояние StateB context.Request(); // Переход в состояние StateA } } abstract class State { public abstract void Handle(Context context); } class StateA: State { public override void Handle(Context context) { context.State = new StateB(); } } class StateB: State { public override void Handle(Context context) { context.State = new StateA(); } } class Context { public State State { get; set; } public Context(State state) { this.State = state; } public void Request() { this.State.Handle(this); } }

Участники паттерна

    State : определяет интерфейс состояния

    Классы StateA и StateB - конкретные реализации состояний

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

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

Class Program { static void Main(string args) { Water water = new Water(WaterState.LIQUID); water.Heat(); water.Frost(); water.Frost(); Console.Read(); } } enum WaterState { SOLID, LIQUID, GAS } class Water { public WaterState State { get; set; } public Water(WaterState ws) { State = ws; } public void Heat() { if(State==WaterState.SOLID) { Console.WriteLine("Превращаем лед в жидкость"); State = WaterState.LIQUID; } else if (State == WaterState.LIQUID) { Console.WriteLine("Превращаем жидкость в пар"); State = WaterState.GAS; } else if (State == WaterState.GAS) { Console.WriteLine("Повышаем температуру водяного пара"); } } public void Frost() { if (State == WaterState.LIQUID) { Console.WriteLine("Превращаем жидкость в лед"); State = WaterState.SOLID; } else if (State == WaterState.GAS) { Console.WriteLine("Превращаем водяной пар в жидкость"); State = WaterState.LIQUID; } } }

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

Class Program { static void Main(string args) { Water water = new Water(new LiquidWaterState()); water.Heat(); water.Frost(); water.Frost(); Console.Read(); } } class Water { public IWaterState State { get; set; } public Water(IWaterState ws) { State = ws; } public void Heat() { State.Heat(this); } public void Frost() { State.Frost(this); } } interface IWaterState { void Heat(Water water); void Frost(Water water); } class SolidWaterState: IWaterState { public void Heat(Water water) { Console.WriteLine("Превращаем лед в жидкость"); water.State = new LiquidWaterState(); } public void Frost(Water water) { Console.WriteLine("Продолжаем заморозку льда"); } } class LiquidWaterState: IWaterState { public void Heat(Water water) { Console.WriteLine("Превращаем жидкость в пар"); water.State = new GasWaterState(); } public void Frost(Water water) { Console.WriteLine("Превращаем жидкость в лед"); water.State = new SolidWaterState(); } } class GasWaterState: IWaterState { public void Heat(Water water) { Console.WriteLine("Повышаем температуру водяного пара"); } public void Frost(Water water) { Console.WriteLine("Превращаем водяной пар в жидкость"); water.State = new LiquidWaterState(); } }

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

Для того, чтобы правильно использовать паттерны Состояние и Стратегия в ядре Java приложений, важно для Java-программистов четко понимать разницу между ними. Хотя оба шаблона, Состояние и Стратегия, имеют схожую структуру, и оба основаны на принципе открытости/закрытости, представляющие ”O” в SOLID принципах , они совершенно разные по намерениям . Паттерн Стратегия в Java используется для инкапсуляции связанных наборов алгоритмов для обеспечения гибкости исполнения для клиента. Клиент может выбрать любой алгоритм во время выполнения без изменения контекста класса, который использует объект Strategy . Некоторые популярные примеры паттерна Стратегия – это написание кода, который использует алгоритмы, например, шифрование, сжатие или сортировки. С другой стороны, паттерн Состояние позволяет объекту вести себя по-разному в разном состоянии. Поскольку в реальном мире объект часто имеет состояния, и он ведет себя по-разному в разных состояниях, например, торговый автомат продает товары только если он в состоянии hasCoin , он не продает до тех пор пока вы не положите в него монету. Сейчас вы можете ясно видеть разницу между паттернами Стратегия и Состояние, это различные намерения. Паттерн Состояние помогает объекту управлять состоянием, тогда как паттерн Стратегия позволяет выбрать клиенту другое поведение. Еще одно отличие, которое не так легко увидеть, это кто управляет изменением в поведении. В случае паттерна Стратегия, это клиент, который предоставляет различные стратегии к контексту, в паттерне Состояние переходом управляет контекст или состояние объекта самостоятельно. Кроме того, если вы управляете изменениями состояний в объекте Состояние самостоятельно, должна быть ссылка на контекст, например, в торговом автомате должна быть возможность вызвать метод setState() для изменения текущего состояния контекста. С другой стороны, объект Стратегия никогда не содержит ссылку на контекст, сам клиент передает Стратегию своего выбора в контекст. Разница между паттернами Состояние и Стратегия один из популярных вопросов о паттернах Java на интервью , в этой статье о паттернах Java мы подробней рассмотрим это. Мы будем исследовать некоторые сходства и различия между паттернами Стратегия и Состояние в Java, которые помогут вам улучшить ваше понимание этих паттернов.

Сходства между паттернами Состояние и Стратегия

Если вы посмотрите на UML-диаграмму паттернов Состояние и Стратегия, можно заметить, что оба выглядят похоже друг на друга. Объект, который использует Состояние для изменения своего поведения известен как Context -объект, аналогично объект, который использует Стратегию чтобы изменить свое поведение упоминается как Context -объект. Запомните, что клиент взаимодействует с Context -объектом. В случае паттерна Состояние контекст делегирует методы вызова объекту Состояние, который удерживается в виде текущего объекта, а в случае паттерна Стратегия контекст использует объект Стратегии в качестве параметра или предоставляется во время создания контекста объекта. UML диаграмма паттерна Состояние в Java
Эта UML диаграмма для паттерна Состояние, изображает классическую проблему создания объектно-ориентированного дизайна торгового аппарата в Java. Вы можете видеть, что состояние торгового аппарата представлено с использованием интерфейса, который далее имеет реализацию для представления конкретного состояния. Каждое состояние также имеет ссылки на контекст объекта, чтобы сделать переход в другое состояние в результате действий вызванных в контексте.
Эта UML диаграмма для паттерна Стратегия содержит функциональные реализации сортировок. Поскольку есть много алгоритмов сортировки, этот шаблон проектирования позволяет клиенту выбрать алгоритм при сортировке объектов. На самом деле Java Collection framework использует этот паттерн реализуя метод Collections.sort() , который используется для сортировки объектов в Java. Единственная разница в том, что вместо разрешения клиенту выбирать алгоритм сортировки он позволяет ему указать стратегию сравнения передавая экземпляр интерфейса Comparator или Comparable в Java . Давайте посмотрим на несколько сходств между этими двумя основными шаблонами проектирования в Java:
  1. Оба паттерна, Состояние и Стратегия, делают несложным добавление нового состояния и стратегии не затрагивая контекст объекта, который использует их.

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

  3. Также как контекст объекта начинается с состояния инициализации объекта в паттерне Состояние, контекст объекта также имеет стратегию по умолчанию в случае паттерна Стратегия в Java.

  4. Паттерн Состояние представляет различные поведения в форме различных состояний объекта, в то время как паттерн Стратегия представляет различное поведение в виде различных стратегий объекта.

  5. Оба паттерна, Стратегия и Состояние, зависят от подклассов реализации поведения. Каждая конкретная стратегия расширяет Абстрактную Стратегию, каждое состояние есть подкласс интерфейса или абстрактного класса , который используется для преставления Состояния.

Различия между паттернами Стратегия и Состояние в Java

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

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

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

  4. Реализация Стратегии может быть передана как параметр объекту, который будет использовать ее, например, Collection.sort() принимает Comparator , который является стратегией. С другой стороны, состояние является частью самого контекста объекта, и в течение долгого времени контекст объекта переходит из одного состояния в другое.

  5. Хотя и Стратегия и Состояние следуют принципу открытости/закрытости, Стратегия также следует Принципу Единственной Обязанности так как каждая Стратегия содержит индивидуальный алгоритм, различные стратегии независимы друг от друга. Изменение одной стратегии не требует изменения другой стратегии.

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

  7. Порядок перехода состояния хорошо определен в паттерне Состояние, такого требования нет к паттерну Стратегия. Клиент волен в выборе любой реализации Стратегии на его выбор.

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

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

Это все про разницу между паттернами Состояние и Стратегия в Java . Как я сказал, оба выглядят похоже в своих классах и UML диаграммах, оба обеспечивают открыто/закрытый принцип и инкапсулируют поведение. Используйте паттерн Стратегия для инкапсулирования алгоритма или стратегии, который предоставляется контексту во время выполнения, возможно как параметр или составной объект и используйте паттерн Состояние для управления переходами между состояниями в Java. Оригинал