Паттерн проектирования «Наблюдатель»

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

Данный паттерн применяется, если:

  • в программе присутствует, как минимум один объект, который способен рассылать сообщения;
  • в программе присутствует, как минимум несколько объектов-получателей сообщения и их состав и количество могут сильно изменяться в процессе работы;
  • нет надобности очень сильно связывать взаимодействующие объекты, что полезно для повторного использования;
  • нет субъекту нет смысла беспокоиться о том, что делают подписчики с той информацией, которую он рассылает.

Пример, показанный в данном рецепте очень наглядно показывает очень ценный и интересный прием, позволяющий модифицировать поведение компонентов программы прямо во время ее выполнения, и он может быть легко использован вами в своих приложениях.

 

aquaratixc

Программист-самоучка и программист-любитель

One thought to “Паттерн проектирования «Наблюдатель»”

  1. Большое спасибо за статью, очень познавательно.
    Кстати, Мне кажется вы незаслуженно обделили вниманием те инструменты которые уже имеются в стандартной библиотеке для реализации этого шаблона, Я говорю про std.signals.
    К сожалению малый опыт программирования, и слабое знание английского языка не позволяют мне подготовить какой либо более менее полезный материал по это теме. Но именно Вы своей статьей обратили на него мое внимание, за что Я Вам благодарен.

Добавить комментарий