Шаблон Observer на PHP
Observer(Наблюдатель) - является поведенческим шаблоном проектирования. Является достаточно популярным шаблоном проектирования, но, при этом, очень прост в реализации. Данный шаблон предполагает зависимость между объектами "один ко многим" так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически. Таким образом, в шаблоне наблюдаются две роли: субъект и слушатель. Шаблон "Наблюдатель"определяется следующими свойствами:
- существует, как минимум, один субъект, рассылающий сообщения;
- имеется не менее одного получателя сообщений, причём их количество и состав могут изменяться во время работы приложения;
- нет надобности очень сильно связывать взаимодействующие объекты, что полезно для повторного использования.
Шаблон подходит для любого сценария, в котором требуется использование push-уведомлений. Очень часто его можно заметить в системах пользовательского интерфейса.
Аналогия
Самая банальная аналогия - подписка на рассылки какого-либо сайта. Когда на сайте появляется новый материал, то всем подписчикам рассылается e-mail-уведомление о появлении нового материала, а подписчики уже принимают решение просматривать новый материал или нет.
UML-диаграмма
Типовая UML-диаграмма выглядит следующим образом:

На ней присутствуют следующие сущности:
IObservable(наблюдаемый) — интерфейс, определяющий методы для добавления, удаления и оповещения наблюдателей;
IObserver(наблюдатель) — интерфейс, с помощью которого наблюдатель получает оповещение;
ConcreteObservable — конкретный класс, который реализует интерфейс IObservable;
ConcreteObserver — конкретный класс, который реализует интерфейс IObserver.
Код шаблона
Фактически, для реализации шаблона "Наблюдатель" необходимо описать два интерфейса с необходимыми методами: IObservable и IObserver. Код интерфейсов:
interface IObservable {
/**
* Добавление нового наблюдателя
*
* @param IObserver $instance
* @return bool
*/
public function attach(IObserver $instance);
/**
* Удаление имеющегося наблюдателя
*
* @param IObserver $instance
* @return bool
*/
public function detach(IObserver $instance);
/**
* Оповещение всех наблюдателей, через вызов у него метода update
*
*/
public function notify();
}
interface IObserver {
/**
* Будет вызван у каждого наблюдателя в notify()
*
* @param IObservable $instance
* @return mixed
*/
public function update(IObservable $instance);
}Реализация методов, указанных в интерфейсах ложится на классы, где эти самые интерфейсы и будут использованы. Например, используя данный шаблон проектирования, мы будем следить за погодой. Допустим, у нас есть класс, который генерирует погоду и несколько слушателей, которые подписаны на обновление прогноза погоды. В коде это выглядит следующим образом:
class WeatherGenerator implements IObservable {
/**
* Массив наблюдателей
*
* @var IObserver[]
*/
private $observers = array();
/**
* Температура
*
* @var float
*/
private $temperature;
/**
* Давление
*
* @var float
*/
private $pressure;
public function attach(IObserver $instance) {
foreach ($this->observers as $observer) {
if ($instance === $observer) {
return false;
}
}
$this->observers[] = $instance;
return true;
}
public function detach(IObserver $instance) {
foreach ($this->observers as $key => $observer) {
if ($instance === $observer) {
unset($this->observers[$key]);
return true;
}
}
return false;
}
public function notify() {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function setParams($aTemperature, $aPressure) {
$this->temperature = $aTemperature;
$this->pressure = $aPressure;
$this->notify();
}
public function getTemperature() {
return $this->temperature;
}
public function getPressure() {
return $this->pressure;
}
}
class WeatherListener1 implements IObserver {
private $currentTemperature;
private $currentPressure;
public function update(IObservable $instance) {
$this->currentTemperature = $instance->getTemperature();
$this->currentPressure = $instance->getPressure();
$this->display();
}
public function display() {
echo '<b>WeatherListener1</b> Текущая температура: '.$this->currentTemperature.'; Текущее давление: '.$this->currentPressure.'<br/>';
}
}
class WeatherListener2 implements IObserver {
private $currentTemperature;
private $currentPressure;
public function update(IObservable $instance) {
$this->currentTemperature = $instance->getTemperature();
$this->currentPressure = $instance->getPressure();
$this->display();
}
public function display() {
echo '<b>WeatherListener2</b> Текущая температура: '.$this->currentTemperature.'; Текущее давление: '.$this->currentPressure.'<br/>';
}
}Ну и добавим код приложения:
$wl1 = new WeatherListener1();
$wl2 = new WeatherListener2();
$weatherGenerator = new WeatherGenerator();
$weatherGenerator->attach($wl1);
$weatherGenerator->attach($wl2);
$weatherGenerator->setParams(14, 770);
$weatherGenerator->setParams(16, 750);
$weatherGenerator->detach($wl1);
$weatherGenerator->setParams(17, 745);Результат работы скрипта:
WeatherListener1 Текущая температура: 14; Текущее давление: 770
WeatherListener2 Текущая температура: 14; Текущее давление: 770
WeatherListener1 Текущая температура: 16; Текущее давление: 750
WeatherListener2 Текущая температура: 16; Текущее давление: 750
WeatherListener2 Текущая температура: 17; Текущее давление: 745
Что в итоге? У нас есть два объекта классов, реализующих интерфейс слушателя(WeatherListener1, WeatherListener2) и один объект класса, генерирующего погоду, который также может оповещать об изменениях погоды(WeatherGenerator). Изначально оба слушателя оповещаются об изменениях погоды, но после второго изменения один из слушателей перестает оповещаться о погоде. Реализация шаблона "Наблюдатель" продемонстрирована в действии.
Как улучшить реализацию?
При анализе шаблона можно сделать вывод, что реализация интерфейса Observable будет всегда выглядеть одинаково. В таком случае целесообразно этот код вынести в отдельный класс и от него наследоваться. Такой подход имеет один небольшой минус - создается ненужная цепочка наследования. Но в php 5.4.0 появилось идеальное, более прогрессивное решение для таких случаев - трейты. Можно и нужно создать трейт, который будет реализовывать интерфейс Observable и впоследствии использовать этот трейт в классе, за которым нужно следить. Код трейта в таком случае будет таким:
trait TObservable {
/**
* Массив наблюдателей
*
* @var TObserver[]
*/
private $observers = array();
public function attach(IObserver $instance) {
foreach ($this->observers as $observer) {
if ($instance === $observer) {
return false;
}
}
$this->observers[] = $instance;
return true;
}
public function detach(IObserver $instance) {
foreach ($this->observers as $key => $observer) {
if ($instance === $observer) {
unset($this->observers[$key]);
return true;
}
}
return false;
}
public function notify() {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
}А использовать его следует таким образом:
class WeatherGenerator implements IObservable {
use TObservable;
...
}В таком случае, UML-диаграмма реализации будет следующей:
Comments