Шаблон Listener.
1. Зависимость классов в шаблоне Model-View-Controller.
1.1. Односторонняя зависимость на примере модели шахмат (из прошлой лекции по MVC).
В модели шахмат мы создали классы Model и View. В классе View (отображение фигур на экране) есть поле, указывающее на модель:
Model * model;
метод moveDone:
void moveDone() {
model->move( x1, y1, x2, y2 );
update();
}
и метод для отрисовки фигур – update.
В этом случае класс View зависит от класса Model, но не наоборот, то есть Model о классе View ничего "не знает". А значит нет циклов в зависимостях классов друг от друга. При отсутствии таких циклов программа является более гибкой, и в ней легче заменять одни классы другими для изменения или улучшения программы.
1.2. Двусторонняя зависимость на примере модели часов.
Аналогично предыдущему примеру, создадим классы ClockModel и ClockView для модели и отображения часов соответственно.
class ClockModel {
public:
int hour;
int minute;
int second;
ClockView * view;
void tick() { //отсчет времени каждую секунду
... //обновляем hour, minute, second
view->update( this );
}
};
class ClockView {
public:
void update( ClockModel * m ) {
//отрисовка часов по полям hour, minute, second из model
}
};
Видно что класс ClockView зависит от ClockModel, так при отрисовке необходимо обращаться к полям hour, minute и second, а класс ClockModel зависит от ClockView, так как вызывает у него метод update. Такая взаимная зависимость усложняет возможность замены классов и изменение программы, тем более другими программистами.
2. Шаблон Listener (слушатель).
2.1. Модель с одним слушателем.
Для разрешения циклических зависимостей классов существует шаблон Listener (он же Observer, он же Publish/Subscribe). С помощью этого шаблона можно реализовать классы так, что один класс может в некотором смысле "прослушивать" другой класс, то есть реагировать на происходящие в нем события.
Изменим наши классы ClockModel и ClockView таким образом, чтобы возможна была следующая запись:
ClockModel model;
ClockView view;
model.setListener( view ); //устанавливаем view слушателем model
Теперь view является наблюдателем, а model – объектом наблюдения.
Если мы хотим устанавливать в качестве слушателей различные классы реализуем виртуальный класс Listener:
class Listener {
public:
virtual void update( ClockModel * model ) = 0;
};
И наследуем от него классы отрисовки часов:
class AnalogClockView: public Listener {
public:
void update( ClockModel * model ) {
... //отрисовка стрелочных часов
}
};
class DigitalClockView: public Listener {
public:
void update( ClockModel * model ) {
... //отрисовка цифровых часов
}
};
А класс ClockModel будет реализован следующим образом:
class ClockModel {
public:
int hour;
int minute;
int second;
void tick() { //отсчет времени каждую секунду
... //обновляем hour, minute, second
listener->update( this );
}
void setListener( Listener * l ) {
listener = l;
}
private:
Listener * listener;
};
Теперь, наследуясь от класса Listener, и перегружая чисто виртуальный метод update, мы можем создавать новые классы для отображения часов.
2.2. Модель с несколькими слушателями.
Если нам одновременно хочется отображать на экране и аналоговые, и цифровые часы, имея при этом одну модель часов, добавим в ClockModel контейнер для слушателей, функции для их добавления, удаления и оповещения:
class ClockModel {
public:
int hour;
int minute;
int second;
void tick() { //отсчет времени каждую секунду
... //обновляем hour, minute, second
notifyAll(); //сообщаем всем слушателям, о том что время изменилось
}
void addtListener( Listener * l ) {
listeners.push_back( l );
}
void removeListener( Listener * l ) {
listeners.erase( l );
}
private:
std::list < Listener * > listeners; //контейнер для слушателей
void notifyAll() { //обновление всех слушателей
for ( std::list < Listeners * >::iterator it = listeners.begin(); it != listners.end(); it++ ) {
it->update( this );
}
}
};
Note: Если мы хотим, чтобы каждый слушатель был указан не больше одного раза, то лучше использовать контейнер std::set.
С новым классом ClockModel возможен вывод двух часов одновременно:
ClockModel model;
AnalogClockView aView;
DigitalClockView dView;
model.addListener( aView );
model.addListener( dView );
В рассмотренных примерах зависимости между классами ClockView и ClockModel по прежнему остаются в обе стороны, но они неравнозначны. Класс ClockModel знает о классе отображения только то, что в нем есть метод update, то есть что он наследован от класса Listener.
2.3. Изменение полей класса ClockModel внешними функциями.
еализуем в классе AnalogClockView фунуцию для изменения времени пользователем:
class AnalogClockView: public Listener {
public:
void update( ClockModel * model ) {
... //отрисовка стрелочных часов
}
void userChangeTime( ClockModel * model ) {
model->hour = ...
model->minute = ...
model->second = ...
}
};
Возникают две проблемы. Во-первых время можно поменять на любое, даже на некорректное. Во-вторых при изменении времени методом userChangeTime остальные наблюдатели, подключенные к модели, не узнают об этом, и измененное время отрисуется только на аналоговых часах, из которых был вызван метод userChangeTime.
Для решения этих двух проблем поля hour, minute и second сделаем закрытыми, и добавим в класс ClockModel сеттеры и геттеры для этих полей:
class ClockModel {
public:
...
void setHour( int h ) {
if ( ( h >=0 ) && ( h < 24 ) ) {
hour = h;
notifyAll(); //сообщаем всем о том, что время изменилось
}
}
int getHour() {
return hour;
}
...
private:
std::list < Listener * > listeners;
int hour;
int minute;
int second;
...
};
Тогда функция userChangeTime будет выглядеть следующим образом:
void userChangeTime( ClockModel * model ) {
model->setHour( ... );
model->setMinute( ... );
model->setSecond( ... );
}
Однако, при изменении времени пользователем функция notifyAll() вызовется три раза подряд, что не очень хорошо. Чтобы избежать этого, вызов функции notifyAll можно убрать из сеттеров, саму функцию сделать публичной и вызвать её по завершению функции userChangeTime:
void userChangeTime( ClockModel * model ) {
model->setHour( ... );
model->setMinute( ... );
model->setSecond( ... );
model->notifyAll();
}
Теперь вся ответственность за вызов метода notifyAll лежит на программисте, вызывающем сеттер, так как notifyAll не будет вызываться автоматически.
2.4. Проблема удаления слушателей.
В контейнере listeners хранятся указатели на слушателей, а значит если какой-то из слушателей будет удален, указатель будет указывать на несуществующий объект, и при попытке вызвать у него метод update, может произойти ошибка. Чтобы избежать такого, добавление и удаление слушателей можно сделать автоматическим:
class Listener {
public:
virtual void update( ClockModel * model ) = 0;
Listener( ClockModel * m ) {
model = m;
model->addListener( this );
}
~Listener() {
model->removeListener( this );
}
private:
ClockModel * model;
};
Тогда для вывода на экран аналоговых и цифровых часов будет достаточно написать:
ClockModel model;
AnalogClockView aView( model );
DigitalClockView dView( model );
3. Прочие проблемы использования шаблона Listener.
3.1. Проблема большого числа различных слушателей.
ассмотрим программу с графическим интерфейсом и большим числом различных элементов управления: кнопками, полосами прокрутки, текстовыми полями и т.д. азные элементы должны реагировать на разные события, такие как нажатие клавиши, нажатие левой кнопки мышки, изменение позиции мышки и прочее. Использование шаблона, рассмотренного выше, будет неэффективно, так как при каком-либо событии функция notifyAll будет оповещать об изменении всех, в то числе и тех слушателей, на которых произошедшее событие сказаться не может. То есть, будет совершаться много лишней работы. Эту проблему можно решить двумя способами:
- Завести несколько массивов для разных типов слушателей (используется в Java).
Тогда в классе модели будут различные методы для добавления и удаления различных слушателей. Например addMouseMoveListener, addMouseRightClickListener и т.д.
- При добавлении слушателя, вторым параметром указывать его тип:
addListener( Listener * listener, ListenerType type );
Тип слушателей ListenerType удобней всего сделать перечислимым:
enum ListenerType { MouseMoveListener, MouseRightClickListener, ... };
В единственном массиве listener необходимо будет хранить указатель на слушателя и его тип. По типу слушателя можно будет сказать, необходимо ли вызывать у него update при данном событии или нет.
3.2. Проблема большой модели.
Пусть у нас есть большая и сложная модель, например модель компьютерной игры с картой. Тогда перерисовка всей карты методом update( Model* ) при каком-либо событии будет занимать значительное время. Чтобы ускорить процесс можно перерисовывать только видимую (активную) область карты, но для этого необходимо предавать методу update не только модель, но и например, границы активной области. То есть, при большой и сложной модели не всегда хорошо передавать её в методу update целиком.