Введение
«Наблюдатель» (observer) – один из часто используемых шаблонов (паттернов) проектирования. Также известен как «подчинённые» (dependents) и «издатель-подписчик» (publisher-subscriber). Определяет зависимость типа «один ко многим» между объектами таким образом, что при возникновении некоторого события в одном из объектов, все объекты, подписавшиеся на оповещения об этом событии, извещаются о его возникновении.
В качестве иллюстрации, приведем хрестоматий пример, описывающий принцип работы шаблона: однажды оформив подписку на некое печатное издание, вы будете получать его новые выпуски каждый раз при их издании, пока не откажетесь от подписки.
Шаблон «наблюдатель» позволяет организовать простое, прозрачное и логичное взаимодействие большого количества объектов различных типов и при этом, что очень важно, сделать зависимости между ними достаточно слабыми. Это обстоятельство позволяет избежать сильного связывания классов, что делает программу более гибкой, и разработчик получает возможность использовать в новых проектах ранее написанные классы без внесения в них существенных модификаций.
Прежде всего, договоримся о терминах, используемых в статье.
- «Событием» будем называть всякое явление (создание экземпляра класса, изменение значения переменной, переход объекта из одного состояния в другое, выполнение логического условия, удаление экземпляра класса), которое может произойти (а может и не произойти) при осуществлении определенной совокупности условий. Каждое событие имеет уникальный идентификатор – имя, который позволяет однозначно отличить одно событие от другого и узнать, какое именно явление имело место произойти в программе.
- «Слушателем событий» будем называть объект, который получает в той или иной форме уведомления о возникновении событий в другом объекте.
- «Диспетчером событий» назовем объект, способный уведомлять другие объекты о возникновении внутри себя некоторых событий (об изменении своего состояния).
Любой объект может стать диспетчером событий: для этого он должен быть унаследован от класса EventDispatcher
, который мы опишем в статье. После этого, другие объекты смогут подписываться на любые события диспетчера и отписываться от них с помощью его публичных методов AddEventListener
и RemoveEventListener
соответственно. Диспетчер событий в свою очередь будет вызывать частный метод DispatchEvent
в момент возникновения события и тем самым – оповещать слушателей, подписавшихся на событие о его возникновении. Рассмотрим каждый из указанных методов подробно.
Подписка на события
Метод AddEventListener
подписывает объект-слушатель, определяющийся указателем listener
на некое событие с именем event
и сообщает диспетчеру, что при возникновении этого события нужно вызвать для слушателя метод, определяющийся указателем action
. В данной статье мы вводим ограничение, согласно которому action
указывает на метод, не возвращающий значений и не принимающий параметров. Метод AddEventListener
объявляется с модификатором доступа public
, так как будет вызываться слушателями. Он также не будет возвращать значений.
Следует понимать, что для того, чтобы передать в функцию AddEventListener
указатель на какой-либо метод слушателя, нужно указать имя типа слушателя. Очевидно, что мы не знаем заранее, объекты каких типов будут подписываться на события диспетчера, поэтому мы сделаем метод AddEventListener
шаблонным. Единственным параметром шаблона будет имя типа объекта-слушателя. Таким образом, объявление класса EventDispatcher
и его метода AddEventListener
будет выглядеть следующим образом:
class EventDispatcher
{
public:
template <typename ListenerType>
void AddEventListener (
string event,
ListenerType* listener,
void ( ListenerType ::* action ) ( void )
);
};
В контексте данной статьи мы будем называть «подпиской» экземпляры класса Subscription
. Они будут иметь три члена: event
, listener
и action
. Подписки будут содержать в себе информацию о том, что объект listener
подписывается на событие с именем event
, которое может произойти в диспетчере, а также, что в случае возникновения указанного события должен быть вызван метод action
для объекта listener
. Метод AddEventListener
будет создавать новый экземпляр класса Subscription
и сохранять указатель на него в векторе subscriptions
, являющимся членом класса EventDispatcher
, а впоследствии – искать в нем соответствующие подписки на возникающие события, и извещать слушателей путем вызова указанных в подписках методов.
Класс Subscription
также будет шаблонным, так как заранее мы не можем сказать, указатели на объекты какого типа данных он будет хранить. Шаблон будет иметь только один параметр – имя типа объекта-слушателя. Конструктор класса принимает три параметра, и записывает их в свои соответствующие приватные члены. Описание класса Subscription
будет выглядеть следующим образом:
template <typename ListenerType>
class Subscription
{
private:
string event;
ListenerType* listener;
void ( ListenerType ::* action ) ( void );
public:
Subscription (
string event,
ListenerType* listener ,
void ( ListenerType ::* action ) ( void )
)
{
this->event = event;
this->listener = listener;
this->action = action;
};
};
Метод AddEventListener
будет создавать объект класса Subscription
и сохранять указатель на него в векторе subscriptions
класса EventDispatcher
. Реализация метода будет следующей:
template <typename ListenerType>
void EventDispatcher::AddEventListener (
string event,
ListenerType* listener,
void ( ListenerType ::* action ) ( void )
)
{
Subscription <ListenerType>* subscription = new Subscription <ListenerType> (
event,
listener,
action
);
this->subscriptions.push_back ( subscription );
};
На данном шаге мы сталкиваемся с небольшой проблемой. Каким образом хранить указатели на подписки? Какого типа будут элементы вектора subscriptions
. Если бы мы решили хранить в нем указатели на объекты типа Subscription
, все подписчики должны были бы быть объектами одного, заранее определенного типа данных, так как класс Subscription
– шаблонный. Решением проблемы будет создание базового класса SubscriptionBase
и наследование от него Subscription
. Запишем объявление базового класса и скорректируем заголовок наследника.
class SubscriptionBase
{
};
template <typename ListenerType> class Subscription : public SubscriptionBase
{
. . .
};
Теперь добавим в класс EventDispatcher
новый приватный член – вектор subscriptions
, хранящий указатели на объекты SubscriptionBase
– подписки слушателей на события диспетчера:
class EventDispatcher
{
private:
vector <SubscriptionBase*> subscriptions;
. . .
};
Оправка событий
Наш диспетчер событий теперь умеет регистрировать подписчиков и сохранять подписки с помощью метода AddEventListener
. Далее мы рассмотрим метод DispatchEvent
. Он предусмотрен для вызова диспетчерами событий – наследниками класса EventDispatcher
в момент возникновения в них некоторого события, поэтому он имеет модификатор доступа protected
. Метод принимает единственный параметр – имя события, о возникновении которого требуется известить подписчиков. Далее он перебирает в цикле все подписки – элементы вектора subscriptions
и выбирает те из них, которые оформлены на событие с именем event
, после чего вызывает для подписавшихся объектов listener
метод action
.
Возникает вопрос, каким образом мы узнаем, на какое событие оформлена та или иная подписка, хранящаяся в векторе subscriptions
. Ведь он хранит указатели на объекты типа SubscriptionBase
, которые не имеют члена event
. Для решения этой проблемы мы опишем в SubscriptionBase
виртуальный метод GetEventName
, который будет перегружен в Subscription
. Он будет возвращать строку, соответствующую имени события, на которое оформлена подписка. Классы SubscriptionBase
и Subscription
будет иметь вид:
class SubscriptionBase
{
public:
virtual string GetEventName ( void ) const
{
};
};
template <typename ListenerType> class Subscription: public SubscriptionBase
{
private:
. . .
virtual string GetEventName ( void ) const
{
return this->event;
};
. . .
}
Проблема, связанная с выяснением, на какое событие оформлена подписка, решена. Теперь метод DispatchEvent
сможет выбрать из всего множества подписок только нужные. Далее он должен будет вызвать для подписчика listener
метод action
. Но опять-таки этой информацией класс SubscriptionBase
не обладает. По аналогии мы опишем в нем виртуальный метод Invoke
, который, будучи перегруженным в Subscription
будет вызывать метод action
для слушателя listener
. Окончательный вид класса SubscriptionBase
будет следующим:
class SubscriptionBase
{
public:
virtual string GetEventName ( void ) const
{
};
virtual void Invoke ( void ) const
{
};
};
После добавления в Subscription
перегруженного метода Invoke
, работа над этим классом также будет завершена:
template <typename ListenerType>
class Subscription : public SubscriptionBase
{
private:
string event;
ListenerType* listener;
void ( ListenerType ::* action ) ( void );
virtual string GetEventName ( void ) const
{
return this->event;
};
virtual void Invoke ( void ) const
{
( this->listener ->* this->action ) ( );
};
public:
Subscription (
string event,
ListenerType* listener,
void ( ListenerType ::* action ) ( void )
)
{
this->event = event;
this->listener = listener;
this->action = action;
};
};
Имея в нашем арсенале методы GetEventName
и Invoke
, описанные в SubscriptionBase
и перегруженные в Subscription
реализация метода DispatchEvent
будет тривиальной:
void EventDispatcher::DispatchEvent ( const string event ) const
{
for ( int key = 0 ; key < this->subscriptions.size() ; key++ )
{
if ( this->subscriptions[key]->GetEventName() == event )
{
this->subscriptions[key]->Invoke();
};
};
};
Описка от событий
Описанный нами класс EventDispatcher
умеет подписывать слушателя на произвольное событие c помощью метода AddEventListener
, а также «отправлять» события с помощью метода DispatchEvent
. Теперь нам осталось реализовать последний метод, который бы отменял подписку на то или иное событие, иными словами – отписывал бы слушателя от события. Он будет носить имя RemoveEventListener
и принимать параметры тех же типов, что и AddEventListener
. Метод RemoveEventListener
перебирает в цикле все подписки из вектора subscriptions
и удаляет те из них, значения полей event
, listener
, action
которых равны значениям соответствующих параметров, переданных в метод.
template <typename ListenerType>
void EventDispatcher::RemoveEventListener (
string event,
ListenerType* listener,
void ( ListenerType ::* action ) ( void )
)
{
for ( int key = 0 ; key < this->subscriptions.size() ; ++key )
{
Subscription<ListenerType>* subscription =
dynamic_cast < Subscription <ListenerType>* > ( this->subscriptions[key] );
if ( subscription && subscription->listener == listener && subscription->action == action )
{
this->subscriptions.erase ( this->subscriptions.begin() + key );
};
};
};
На этом разработка класса EventDispatcher
завершается. По ссылкам доступны файлы EventDispatcher.h
и EventDispatcher.cpp
.
Пример использования
Для проверки работы нашего класса напишем несложный тест. Экземпляр класса Foo
будет диспетчером событий, а экземпляр класса Bar
– слушателем. Сначала мы подпишем слушателя на событие FOO_EVENT_1
и проверим, оповещается ли слушатель о его возникновении. Потом мы отпишем слушателя от этого события и подпишем на новое: FOO_EVENT_2
. Следует обратить внимание на тот факт, что хотя в диспетчере возникает сразу два события, слушатель оповещается только об одном из них, так как в конкретный момент времени он подписан только на одно событие.
#include <iostream>
#include <unistd.h>
#include "EventDispatcher/EventDispatcher.h"
using namespace std;
class Foo : public EventDispatcher
{
public:
void Run ( void )
{
for ( int key = 0 ; key < 5 ; key ++ )
{
this->DispatchEvent ( "FOO_EVENT_1" );
this->DispatchEvent ( "FOO_EVENT_2" );
usleep ( 500000 );
};
};
};
class Bar
{
public:
void EventHandler1 ( void )
{
cout << "FOO_EVENT_1 occuredn";
};
void EventHandler2 ( void )
{
cout << "FOO_EVENT_2 occuredn";
};
};
int main()
{
Foo* foo = new Foo ( );
Bar* bar = new Bar ( );
foo->AddEventListener ( "FOO_EVENT_1" , bar , &Bar::EventHandler1 );
foo->Run();
foo->RemoveEventListener ( "FOO_EVENT_1" , bar , &Bar::EventHandler1 );
foo->AddEventListener ( "FOO_EVENT_2" , bar , &Bar::EventHandler2 );
foo->Run();
return 1;
};
Результат работы программы будет следующим:
FOO_EVENT_1 occured
FOO_EVENT_1 occured
FOO_EVENT_1 occured
FOO_EVENT_1 occured
FOO_EVENT_1 occured
FOO_EVENT_2 occured
FOO_EVENT_2 occured
FOO_EVENT_2 occured
FOO_EVENT_2 occured
FOO_EVENT_2 occured
Использованная литература
- Wikipedia – Наблюдатель (шаблон проектирования)
- http://cpp-reference.ru – Паттерн Observer
- http://www.oodesign.com – Observer Pattern
Автор: K0LYUNYA