В этом небольшом рецепте мы покажем, как легко и просто приготовить паттерн проектирования «Наблюдатель» (или в английском варианте, паттерн «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.
К сожалению малый опыт программирования, и слабое знание английского языка не позволяют мне подготовить какой либо более менее полезный материал по это теме. Но именно Вы своей статьей обратили на него мое внимание, за что Я Вам благодарен.