Паттерн Состояние (State) предназначен для проектирования классов, которые имеют несколько независимых логических состояний. Давайте сразу перейдем к рассмотрению примера.
Допустим, мы разрабатываем класс управления веб-камерой. Камера может находиться в трех Состояниях:
- Не инициализирована. Назовем NotConnectedState;
- Инициализирована и готова к работе, но кадры еще не захватываются. Пусть это будет ReadyState;
- Активный режим захвата кадров. Обозначим ActiveState.
Поскольку мы работаем с паттером Состояние, то лучше всего начать с изображения Диаграммы состояний:
изображение-01
Теперь превратим эту диаграмму в код. Чтобы не усложнять реализацию, код работы с веб-камерами мы опускаем. При необходимости вы сами можете добавить соответствующие вызовы библиотечных функций.
Сразу привожу полный листинг с минимальными комментариями. Далее мы обсудим ключевые детали этой реализации подробнее.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
#include <iostream> #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->changeState( ReadyState::getInstance() ); } Frame getFrame( WebCamera* ) { std::cout << "Получаем текущий кадр..." << 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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. Происходит непосредственное делегирование:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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():
1 2 3 |
void changeState( State* newState ) { m_state = newState; } |
Теперь к реализации конкретных Состояний. Для WebCamera::NotConnectedState достаточно переопределить операции connect() и disconnect():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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; } |
Вот что в результате будет выведено на консоль:
1 2 3 4 5 6 |
Инициализируем камеру... Запускаем видео-поток... Получаем текущий кадр... Current frame Останавливаем видео-поток... Деинициализируем камеру... |
А теперь попробуем спровоцировать ошибку. Вызовем connect() два раза подряд:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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; } |
Вот что из этого получится:
1 2 3 |
Инициализируем камеру... Произошло исключение!!! Деинициализируем камеру... |
Обратите внимание, что камера все же была деинициализирована. Вызов disconnect() произошел в деструкторе WebCamera. Т.е. внутреннее Состояние объекта осталось абсолютно корректным.
Выводы
С помощью паттерна Состояние вы можете однозначно преобразовать Диаграмму состояний в код. На первый взгляд реализация получилась многословной. Однако мы пришли к четкому делению по возможным контекстам работы с основным классом WebCamera. В результате при написании каждого отдельного Состояния мы смогли сконцентрироваться на узкой задаче. А это лучший способ написать ясный, понятный и надежный код.