В этом небольшом рецепте мы покажем, как легко и просто приготовить паттерн проектирования “Наблюдатель” (или в английском варианте, паттерн “Observer”), а чтобы не городить огород из надуманных примеров, мы возьмем простой и доступный пример из книги Э.Фримен “Паттерны проектирования” и создадим свою погодную станцию.
Итак, от нас требуется создать некое API для погодной станции, которая имеет много различных виджетов-информеров. Кроме того, должно быть обеспечено динамическое обновление информации и легкое добавление своих виджетов. Но помимо этого мы должны помнить о том, что погодная станция постоянно измеряет некоторые параметры (температура, влажность и т.д) и от этих данных зависит работа всех без исключения виджетов.
Налицо проблема: при появлении новых данных информация должна обновится на всех виджетах сразу и при этом каждый из этих информеров должен соответствовать некоторому единообразному интерфейсу.
Проблему поможет разрешить паттерн проектирования “Наблюдатель”, официальное определение которого звучит так:
Паттерн Наблюдатель определяет отношение “один-ко-многим” между объектами таким образом, что при изменении состояния одного объекта происходит автоматическое оповещение и обновление всех зависимых объектов.
Также, данный шаблон проектирования использует терминологию субъект (издатель) / наблюдатель (подписчик), которая наглядно показывает соотношения между компонентами паттерна. Субъект – это тот, кто информирует остальные объекты об изменениях, а наблюдатель – тот, кто получает актуальные данные. Кроме того, суъект обычно реализует все те методы, которые обеспечивают столь динамичное поведение, в частности, методы подписывания/отписывания наблюдателей и оповещения.
Вот так выглядит паттерн “Наблюдатель” в виде диаграммы:

А теперь весь код с подробными комментариями и встроенным юнит-тестированием:
module observer;
/* Пример паттерна проектирования "Наблюдатель"
*
* Краткое описание ситуации:
* Требуется создать API для погодной станции, которая имеет кучу разных
* погодных информеров. Кроме того, должна быть возможность легкого добавления
* своих информеров и динамического их отображения.
*
* Официальное определение паттерна:
* "Паттерн Наблюдатель определяет отношение "один-ко-многим" между объектами
* таким образом, что при изменении состояния одного объекта происходит автоматическое
* оповещение и обновление всех зависимых объектов."
*
* Субъект - издатель, тот кто информирует объекты об обновлениях.
* Наблюдатель - подписчик, тот кто получает данные от издателя.
*
*
* Мои пояснения:
* паттерн реализован в самом простом виде, кроме того,
* класс статистики пришлось написать самому...
*/
// интерфейс, реализуемый субъектом
public interface Subject
{
public void registerObserver(Observer observer);
public void removeObserver(Observer observer);
public void notifyObservers();
}
// интерфейс, реализуемый всеми наблюдателями
public interface Observer
{
public void update(float temperature, float humidity, float pressure);
}
// интерфейс, реализуемый всеми объектами, имеющими визуальное представление
public interface DisplayElement
{
public void display();
}
// класс субъекта
public class WeatherData : Subject
{
private:
Observer[] observers;
float temperature;
float humidity;
float pressure;
public:
// список наблюдателей в начале пустой
this()
{
observers = [];
}
// подписать некоторого наблюдателя на обновления
void registerObserver(Observer observer)
{
observers ~= observer;
}
// отписать наблюдателя от обновлений
void removeObserver(Observer observer)
{
// вспомогательная процедура удаления элемента из массива
T[] remove(T)(T[] x, T y)
{
T[] tmp;
foreach (elem; x)
{
if (elem != y) tmp ~= elem;
}
return tmp;
}
observers = remove(observers, observer);
}
// уведомить подписчиков об обновлении
void notifyObservers()
{
try
{
foreach (observer; observers)
{
observer.update(temperature, humidity, pressure);
}
}
catch
{
}
}
// данные измерений погоды изменились
public void measurementsChanged()
{
notifyObservers();
}
// имитация поставки данных от новых измерений
public void setMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
import std.stdio;
// текущие погодные условия (первый наблюдатель)
public class CurrentConditionsDisplay : Observer, DisplayElement
{
private:
float temperature;
float humidity;
Subject weatherData;
public:
this(Subject weatherData)
{
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
void update(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
display();
}
void display()
{
writefln("Current conditions: %f F degrees and %f %% humidity", temperature, humidity);
}
}
// статистика погоды (второй наблюдатель)
public class StatisticsDisplay : Observer, DisplayElement
{
private:
float[] temperatures;
Subject weatherData;
float minimum(float[] arr)
{
if (arr.length <= 1)
{
return arr.length == 0 ? 0.0 : arr[0];
}
else
{
import std.algorithm : reduce, min;
return reduce!min(arr);
}
}
float maximum(float[] arr)
{
if (arr.length <= 1)
{
return arr.length == 0 ? 0.0 : arr[0];
}
else
{
import std.algorithm : reduce, max;
return reduce!max(arr);
}
}
float average(float[] arr)
{
if (arr.length == 0) return 0;
else
{
import std.algorithm : sum;
return sum(arr) / arr.length;
}
}
public:
this(Subject weatherData)
{
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
void update(float temperature, float humidity, float pressure)
{
this.temperatures ~= temperature;
display();
}
void display()
{
writefln("Min/Max/Avg : %f/%f/%f", minimum(temperatures), maximum(temperatures), average(temperatures));
}
}
unittest
{
writeln("--- Observer test ---");
// создание субъекта
WeatherData weatherData = new WeatherData();
// погодные информеры
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
// изменения погоды
weatherData.setMeasurements(80, 65, 30.4);
weatherData.setMeasurements(82, 70, 29.2);
weatherData.setMeasurements(78, 90, 29.2);
// мы не хотим больше видеть статистику на мониторе
weatherData.removeObserver(statisticsDisplay);
// новые измерения
weatherData.setMeasurements(65, 65, 31.8);
weatherData.setMeasurements(80, 50, 25.2);
weatherData.setMeasurements(76, 70, 25.9);
}
Данный паттерн применяется, если:
- в программе присутствует, как минимум один объект, который способен рассылать сообщения;
- в программе присутствует, как минимум несколько объектов-получателей сообщения и их состав и количество могут сильно изменяться в процессе работы;
- нет надобности очень сильно связывать взаимодействующие объекты, что полезно для повторного использования;
- нет субъекту нет смысла беспокоиться о том, что делают подписчики с той информацией, которую он рассылает.
Пример, показанный в данном рецепте очень наглядно показывает очень ценный и интересный прием, позволяющий модифицировать поведение компонентов программы прямо во время ее выполнения, и он может быть легко использован вами в своих приложениях.
Большое спасибо за статью, очень познавательно.
Кстати, Мне кажется вы незаслуженно обделили вниманием те инструменты которые уже имеются в стандартной библиотеке для реализации этого шаблона, Я говорю про std.signals.
К сожалению малый опыт программирования, и слабое знание английского языка не позволяют мне подготовить какой либо более менее полезный материал по это теме. Но именно Вы своей статьей обратили на него мое внимание, за что Я Вам благодарен.