Для того, чтобы правильно использовать паттерны Состояние и Стратегия в ядре 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:
- Оба паттерна, Состояние и Стратегия, делают несложным добавление нового состояния и стратегии не затрагивая контекст объекта, который использует их.
- Оба из них поддерживают ваш код в соответствии с принципом открытости/закрытости , то есть дизайн будет открыт для расширений, но закрыт для модификации. В случае паттернов Состояние и Стратегия, контекст объекта закрыт для модификаций, введений новых Состояний или новых Стратегий, либо вы не нуждаетесь в модификации контекста другого состояния, или минимальных изменениях.
- Также как контекст объекта начинается с состояния инициализации объекта в паттерне Состояние, контекст объекта также имеет стратегию по умолчанию в случае паттерна Стратегия в Java.
- Паттерн Состояние представляет различные поведения в форме различных состояний объекта, в то время как паттерн Стратегия представляет различное поведение в виде различных стратегий объекта.
- Оба паттерна, Стратегия и Состояние, зависят от подклассов реализации поведения. Каждая конкретная стратегия расширяет Абстрактную Стратегию, каждое состояние есть подкласс интерфейса или абстрактного класса , который используется для преставления Состояния.
Различия между паттернами Стратегия и Состояние в Java
Итак, теперь мы знаем, что паттерны Состояние и Стратегия похожи по структуре, а их намерения различны. Давайте рассмотрим некоторые ключевые различия между этими шаблонами проектирования.- Паттерн Стратегия инкапсулирует набор связанных алгоритмов, и позволяет клиенту использовать взаимозаменяемые поведения несмотря на состав и делегирование во время выполнения, с другой стороны паттерн Состояние помогает классу демонстрировать различные поведения в различных состояниях.
- Следующая разница между паттернами Состояние и Стратегия состоит в том, что Состояние инкапсулирует состояние объекта, тогда как паттерн Стратегия инкапсулирует алгоритм или стратегию. Поскольку состояние связано с объектом оно не может быть повторно использовано, но отделяя стратегию или алгоритм из контекста мы можем использовать его повторно.
- В паттерне Состояние личное состояние может содержать ссылку на контекст для реализации переходов между состояниями, но Стратегия не содержит ссылку на контекст где она используется.
- Реализация Стратегии может быть передана как параметр объекту, который будет использовать ее, например, Collection.sort() принимает Comparator , который является стратегией. С другой стороны, состояние является частью самого контекста объекта, и в течение долгого времени контекст объекта переходит из одного состояния в другое.
- Хотя и Стратегия и Состояние следуют принципу открытости/закрытости, Стратегия также следует Принципу Единственной Обязанности так как каждая Стратегия содержит индивидуальный алгоритм, различные стратегии независимы друг от друга. Изменение одной стратегии не требует изменения другой стратегии.
- Еще одно теоретическое отличие между паттернами Стратегия и Состояние заключается в том, что создатель определяет часть объекта “Как”, например, “Как” объект сортировки сортирует данные, с другой стороны паттерн Состояние определяет части “что” и “когда” в объекте, например, что может объект когда он находится в определенном состоянии.
- Порядок перехода состояния хорошо определен в паттерне Состояние, такого требования нет к паттерну Стратегия. Клиент волен в выборе любой реализации Стратегии на его выбор.
- Некоторые из общих примеров паттерна Стратегия – это инкапсуляция алгоритмов, например, алгоритмы сортировки, шифрования или алгоритм сжатия. Если вы видите, что ваш код должен использовать различные виды связанных алгоритмов, следует подумать об использовании паттерна Стратегия. С другой стороны, распознать возможность использования паттерна Состояние довольно легко, если вам нужно управлять состоянием и переходами между состояниями без большого количества вложенных условных операторов паттерн Состояние – нужный паттерн для использования.
- Последнее, но одно из наиболее важных различий между паттернами Состояние и Стратегия заключается в том, что изменение Стратегии выполняется Клиентом, а изменение Состояния может быть выполнено контекстом или состоянием объекта самостоятельно.
15.02.2016
21:30
Паттерн Состояние (State) предназначен для проектирования классов, которые имеют несколько независимых логических состояний. Давайте сразу перейдем к рассмотрению примера.
Допустим, мы разрабатываем класс управления веб-камерой. Камера может находиться в трех Состояниях:
- Не инициализирована. Назовем NotConnectedState ;
- Инициализирована и готова к работе, но кадры еще не захватываются. Пусть это будет ReadyState ;
- Активный режим захвата кадров. Обозначим ActiveState .
Поскольку мы работаем с паттером Состояние, то лучше всего начать с изображения Диаграммы состояний:
Теперь превратим эту диаграмму в код. Чтобы не усложнять реализацию, код работы с веб-камерами мы опускаем. При необходимости вы сами можете добавить соответствующие вызовы библиотечных функций.
Сразу привожу полный листинг с минимальными комментариями. Далее мы обсудим ключевые детали этой реализации подробнее.
#include
Обращаю внимание на макрос 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 . В результате при написании каждого отдельного Состояния мы смогли сконцентрироваться на узкой задаче. А это лучший способ написать ясный, понятный и надежный код.
Поведенческий шаблон проектирования. Используется в тех случаях, когда во время выполнения программы объект должен менять своё поведение в зависимости от своего состояния. Классическая реализация предполагает создание базового абстрактного класса или интерфейса, содержащего все методы и по одному классу на каждое возможно состояние. Шаблон представляет собой частный случай рекомендации «заменяйте условные операторы полиморфизмом ».
Казалось бы, все по книжке, но есть нюанс. Как правильно реализовать методы не релевантные для данного состояния? Например, как удалить товар из пустой корзины или оплатить пустую корзину? Обычно каждый state-класс реализует только релевантные методы, а в остальных случаях выбрасывает InvalidOperationException .
Нарушение принципа подстановки Лисков на лицо. Yaron Minsky предложил альтернативный подход : сделайте недопустимые состояния непредставимыми (make illegal states unrepresentable)
. Это дает возможность перенести проверку ошибок со времени исполнения на время компиляции. Однако control flow в этом случае будет организован на основе сопоставления с образцом, а не с помощью полиморфизма. К счастью, частичная поддержка pattern matching появилась в C#7 .
Более подробно на примере F# тема make illegal states unrepresentable раскрыта на сайте Скотта Влашина .
Рассмотрим реализацию «состояния» на примере корзины. В C# нет встроенного типа union . Разделим данные и поведение. Само состояние будем кодировать с помощью enum, а поведение отдельным классом. Для удобства объявим атрибут, связывающий enum и соответствующий класс поведения, базовый класс «состояния» и допишем метод расширения для перехода от enum к классу поведения.
Инфраструктура
public class StateAttribute: Attribute { public Type StateType { get; } public StateAttribute(Type stateType) { StateType = stateType ?? throw new ArgumentNullException(nameof(stateType)); } } public abstract class StateПредметная область
Объявим сущность «корзина»:Public interface IHasState
Реализуем по одному классу на каждое состояние корзины: пустую, активную и оплаченную, но не будем объявлять общий интерфейс. Пусть каждое состояние реализует только релевантное поведение. Это не значит, что классы EmptyCartState , ActiveCartState и PaidCartState не могут реализовать один интерфейс. Они могут, но такой интерфейс должен содержать только методы, доступные в каждом состоянии. В нашем случае метод Add доступен в EmptyCartState и ActiveCartState , поэтому можно унаследовать их от абстрактного AddableCartStateBase . Однако, добавлять товары можно только в неоплаченную корзину, поэтому общего интерфейса для всех состояний не будет. Таким образом мы гарантируем отсутствие InvalidOperationException в нашем коде на этапе компиляции.
Public partial class Cart
{
public enum CartStateCode: byte
{
Empty,
Active,
Paid
}
public interface IAddableCartState
{
ActiveCartState Add(Product product);
IEnumerable
Состояния объявлены вложенными (nested
) классами не случайно. Вложенные классы имеют доступ к защищенным членам класса Cart , а значит нам не придется жертвовать инкапсуляцией сущности для реализации поведения. Чтобы не мусорить в файле класса сущности я разделил объявление на два: Cart.cs и CartStates.cs с помощью ключевого слова partial .
Public ActionResult GetViewResult(State
В зависимости от состояния корзины будем использовать разные представления. Для пустой корзины выведем сообщение «ваша корзина пуста». В активной корзине будет список товаров, возможность изменить количество товаров и удалить часть из них, кнопка «оформить заказ» и общая сумма покупки.
Оплаченная корзина будет выглядеть также, как и активная, но без возможности что-либо отредактировать. Этот факт можно отметить выделением интерфейса INotEmptyCartState . Таким образом мы не только избавились от нарушения принципа подстановки Лисков, но и применили принцип разделения интерфейса.
Заключение
В прикладном коде мы можем работать по интерфейсным ссылкам IAddableCartState и INotEmptyCartState , чтобы повторно использовать код, отвечающий за добавление товаров в корзину и вывод товаров в корзине. Я считаю, что pattern matching подходит для control flow в C# только когда между типами нет ничего общего. В остальных случаях работа по базовой ссылке удобнее. Аналогичный прием можно применить не только для кодирования поведения сущности, но и дляСостояние - это поведенческий паттерн, позволяющий динамически изменять поведение объекта при смене его состояния.
Поведения, зависящие от состояния, переезжают в отдельные классы. Первоначальный класс хранит ссылку на один из таких объектов-состояний и делегирует ему работу.
Особенности паттерна на Java
Сложность:
Популярность:
Применимость: Паттерн Состояние часто используют в Java для превращения в объекты громоздких стейт-машин, построенных на операторах switch .
Примеры Состояния в стандартных библиотеках Java:
- javax.faces.lifecycle.LifeCycle#execute() (контролируемый из FacesServlet : поведение зависит от текущей фазы (состояния) JSF)
Признаки применения паттерна: Методы класса делегируют работу одному вложенному объекту.
Аудиоплеер
Основной класс плеера меняет своё поведение в зависимости от того, в каком состоянии находится проигрывание.