Шаблон 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 будет оповещать об изменении всех, в то числе и тех слушателей, на которых произошедшее событие сказаться не может. То есть, будет совершаться много лишней работы. Эту проблему можно решить двумя способами:

3.2. Проблема большой модели.

Пусть у нас есть большая и сложная модель, например модель компьютерной игры с картой. Тогда перерисовка всей карты методом update( Model* ) при каком-либо событии будет занимать значительное время. Чтобы ускорить процесс можно перерисовывать только видимую (активную) область карты, но для этого необходимо предавать методу update не только модель, но и например, границы активной области. То есть, при большой и сложной модели не всегда хорошо передавать её в методу update целиком.