Наблюдатель (Observer) — шаблон проектирования
3 февраля 2020
Шаблон предлагает схему, в которой при изменений объекта другие объекты получают уведомления и могут выполнять различные обработки.
Широкое применение шаблон получил в реализации событий, когда после наступления какого-то события нужно выполнить ряд действий. Например, после оформления заказа и наступления события «Заказ оформлен» необходимо отправить администрации сайта письмо о поступившем заказе.
Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 4 Кб)
Шаблон состоит из двух элементов:
Идея шаблона заключается в том, что Цель добавляет к себе Наблюдателей и сообщает им о своих изменениях. Наблюдатели при получении от Цели соответствующих сообщений выполняют различные действия и обработки.
Рассмотрим классы подробнее.
Когда объект данного класса добавляется в наблюдатели заказа и у заказа срабатывает событие order_updated, то у наблюдателя вызывается одноимённый метод order_updated($orderId, $oldParams, $newParams). Внутри метода проверяется смена статуса заказа и если установился статус Заказ доставлен в пункт выдачи, то покупателю отправляется письмо с уведомлением о доставке заказа в пункт выдачи (в качестве демонстрации письмо по факту не отправляется, а выводится на экран).
Затем в заказ на событие order_updated добавляем наблюдателя, которым выступает анонимная функция-обработчик. Внутри функции проверяется смена статуса заказа и если установился статус Заказ доставлен в пункт выдачи, то эмулируется отправка покупателю SMS с уведомлением о доставке посылки в пункт выдачи (по факту сообщение выводится на экран).
Далее аналогичным образом в заказ на событие order_updated добавляем наблюдателя, но теперь им выступает уведомление о заказе – объект класса OrdersNotifier
В конце скрипта изменяем заказ, устанавливая ему статус Заказ доставлен в пункт выдачи
Как мы помним, после изменения заказа срабатывает событие order_updated и выполняются обработчики всех наблюдателей, добавленных для этого события. В нашем случае выполнятся два обработчика – отправка SMS и письма о приходе заказа в пункт выдачи.
Полный код скрипта test.php
Результат выполнения скрипта test.php
Широкое применение шаблон получил в реализации событий, когда после наступления какого-то события нужно выполнить ряд действий. Например, после оформления заказа и наступления события «Заказ оформлен» необходимо отправить администрации сайта письмо о поступившем заказе.
ПРИМЕР ЗАДАЧИ: в момент прихода заказа в пункт выдачи нужно отправить покупателю SMS и письмо с сообщением о доставке заказа.
Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 4 Кб)
Шаблон состоит из двух элементов:
- Цель – объект, за активностью которого следят объекты-наблюдатели и который отправляет наблюдателям уведомления о своих изменениях;
- Наблюдатель – объект, который следит за целью и принимает в обработку уведомления об изменении цели.
Идея шаблона заключается в том, что Цель добавляет к себе Наблюдателей и сообщает им о своих изменениях. Наблюдатели при получении от Цели соответствующих сообщений выполняют различные действия и обработки.
observer.class.php
Файл содержит абстрактный класс ObserverTarget, представляющий цель наблюдения. С помощью наследования данного класса любой другой класс может стать целью наблюдений. Класс состоит из следующих свойств и методов:- $observers – массив наблюдателей. В ключе элемента массива находится символьный код события, а в значении находится массив объектов-наблюдателей и функций-обработчиков. Символьный код события используется для группировки наблюдателей по типам изменений Цели. Например, если Цель удалилась – уведомления об этом отправляются одной группе наблюдателей, а если Цель поменяла цвет, то уведомления об этом отправляются уже совсем другой группе наблюдателей;
- addObserver($eventCode, $observer) – добавляет в Цель объект-наблюдатель или функцию-обработчик, которые вызываются при срабатывании события $eventCode;
- deleteObserver($eventCode, $observer) – удаляет наблюдателя из Цели;
- event($eventCode, $params = array()) – производит наступление события. Метод ищет для заданного события $eventCode наблюдателей и вызывает у них одноимённую функцию-обработчик. Если указана анонимная функция, то выполняет её. Например, если указан объект-наблюдатель и наступило событие order_created, то у объекта будет вызван метод order_created.
<?
/**
* Абстрактный класc "Цель наблюдения"
*/
abstract class ObserverTarget
{
/**
* Массив наблюдателей. В ключе элемента массива находится символьный код события, а в значении
* находится массив объектов-наблюдателей или функций-обработчиков
* @var array
*/
protected $observers = array();
/**
* Добавляет объект-наблюдатель или функцию-обработчик, которые вызываюется при срабатывании события
* @param string $eventCode - символьный код события, при срабатывании которого вызывается функция-обработчик наблюдателя
* @param object|callable $observer - объект-наблюдатель или анонимная функция-обработчик
* @return bool - вернёт 'true' в случае успешного добавления и 'false' в случае ошибки
*/
public function addObserver($eventCode, $observer)
{
if (isset($this->observers[$eventCode]) == false) {
$this->observers[$eventCode] = array();
}
//Проверяем, что объект-наблюдатель или анонимная функция-обработчик уже добавлены в наблюдатели
foreach ($this->observers[$eventCode] as $addedObserver) {
if ($observer === $addedObserver) {
return false;
}
}
$this->observers[$eventCode][] = $observer;
return true;
}
/**
* Удаляет наблюдателя
* @param string $eventCode - символьный код события
* @param object|callable $observer - объект-наблюдатель или анонимная функция-обработчик
* @return bool - вернёт 'true' в случае удаления и 'false' в случае, если наблюдателя не удалось найти (возможно, его не существовало или он был удалён ранее)
*/
public function deleteObserver($eventCode, $observer)
{
if (isset($this->observers[$eventCode]) == false) {
return false;
}
//Обходим массив наблюдателей в поисках нужного наблюдателя
foreach ($this->observers[$eventCode] as $key => $addedObserver) {
if ($observer === $addedObserver) {
unset($this->observers[$eventCode][$key]);
return true;
}
}
return false;
}
/**
* Наступление события. Ищет для данного события наблюдателей и вызывает у них
* одноимённую функцию-обработчик. Если указана анонимная функция, то выполняет её.
* Например, если указан наблюлюдатель-объект и наступило события 'order_created', то
* у объекта будет вызван метод 'order_created'
*
* @param string $eventCode - символьный код события
* @param array $params - массив аргументов, которые будут переданы в обработичики наблюдателей
* @return int - вернёт количество сработанных обработчиков у наблюдателей
*/
public function event($eventCode, $params = array())
{
$countHandlers = 0;
//Если события в массиве наблюдателей нету, то завершаем работу метода
if (isset($this->observers[$eventCode]) == false) {
return $countHandlers;
}
foreach ($this->observers[$eventCode] as $observer) {
if (is_callable($observer)) {
call_user_func_array($observer, $params);
$countHandlers++;
} else if (is_object($observer) && method_exists($observer, $eventCode)) {
call_user_func_array(array($observer, $eventCode), $params);
} else {
throw new Exception('Ошибка. Для события «'.$eventCode.'» задан наблюдатель с невыполнимым обработчиком');
}
}
return $countHandlers;
}
}
?>
order.classes.php
Содержит следующие классы:- Order – заказ;
- OrdersNotifier – уведомления о заказе.
Рассмотрим классы подробнее.
Order
Представляет заказ и наследуясь от ObserverTarget становится целью наблюдения. Помимо унаследованных свойств и методов, содержит собственные:- $id – ID заказа;
- $params – массив с параметрами (свойствами) заказа;
- Конструктор __construct($id) – создаёт объект заказа;
- loadParams() – загружает параметры заказа из базы данных в свойство params;
- update($params) – изменяет заказ. После сохранения изменений срабатывает событие обновления заказа order_updated (с помощью метода event($eventCode, $params = array()) родительского класса ObserverTarget) и выполняются обработчики наблюдателей, добавленные для данного события;
/**
* Заказ
*/
class Order extends ObserverTarget
{
/**
* ID заказа
* @var string
*/
private int $id;
/**
* Параметры заказа
* @var array
*/
private $params = array();
/**
* Конструктор. Создаёт объект заказа
* @param int $id - ID заказа
*/
public function __construct($id)
{
$this->id = $id;
//Загружаем параметры заказа
$this->loadParams();
}
/**
* Загружает параметры заказа из БД в свойство $params
* @return bool - в случе успешной загрузки вернёт 'true', а в случае ошибки вернёт 'false'
*/
public function loadParams()
{
//Обращаемся к БД для получения акуальных параметров заказа
$this->params = array(
'date_create' => 1580568525,
'sum' => '4500',
'status' => 'В пути',
'address_delivery_full' => 'Россия, Ивановская область, г. Иваново, ул. Соколовая, д. 1',
'address_delivery_short' => 'г. Иваново, ул. Соколовая, д. 1',
);
return true;
}
/**
* Изменение заказа
* @param array $params - массив с параметрами заказа
* @return bool - в случе успешного сохрнения изменений вернёт 'true', а в случае ошибки вернёт 'false'
*/
public function update($params)
{
$oldParams = $this->params;
/*
Проводим каие-то манипуляции, обработки и сохраняем изменения заказа.
Сохранение прошло успешно
*/
$updateSuccess = true;
if ($updateSuccess == true) {
foreach ($params as $field => $value) {
if (isset($this->params[$field])) {
$this->params[$field] = $value;
}
}
//Вызываем событие обновления заказа
$this->event('order_updated', array(
'id' => $this->id,
'old_params' => $oldParams,
'new_params' => $this->params
));
return true;
} else {
return false;
}
}
}
OrdersNotifier
Класс реализует уведомления о заказе и является наблюдателем Заказа. В качестве демонстрации содержит один метод order_updated($orderId, $oldParams, $newParams), представляющий обработчик для события изменения заказа.Когда объект данного класса добавляется в наблюдатели заказа и у заказа срабатывает событие order_updated, то у наблюдателя вызывается одноимённый метод order_updated($orderId, $oldParams, $newParams). Внутри метода проверяется смена статуса заказа и если установился статус Заказ доставлен в пункт выдачи, то покупателю отправляется письмо с уведомлением о доставке заказа в пункт выдачи (в качестве демонстрации письмо по факту не отправляется, а выводится на экран).
/**
* Уведомитель о заказах
*/
class OrdersNotifier
{
/**
* Уведомление об изменении заказа
* @param int $orderId - ID заказа
* @param array $oldParams - массив с параметрами заказа ДО изменения
* @param array $newParams - массив с параметрами заказа ПОСЛЕ изменения
*/
public function order_updated($orderId, $oldParams, $newParams)
{
if ($oldParams['status'] != 'Заказ доставлен в пункт выдачи' && $newParams['status'] == 'Заказ доставлен в пункт выдачи') {
echo 'Отправка письма:<br>
Ваш заказ №'.$orderId.' на сумму '.number_format($newParams['sum'], 0, ',', ' ').' руб. доставлен в пункт выдачи по адресу '.$newParams['address_delivery_short'].'<br>
<br>
';
}
}
}
test.php
Содержит тестовый скрипт, в котором создаётся заказ $order (объект класса Order)://Создаём заказ
$order = new Order(18954);
Затем в заказ на событие order_updated добавляем наблюдателя, которым выступает анонимная функция-обработчик. Внутри функции проверяется смена статуса заказа и если установился статус Заказ доставлен в пункт выдачи, то эмулируется отправка покупателю SMS с уведомлением о доставке посылки в пункт выдачи (по факту сообщение выводится на экран).
//Добавляем для заказ наблюдателя, которым выступает анонимная функция-обработчик
$order->addObserver('order_updated', function($orderId, $oldParams, $newParams){
if ($oldParams['status'] != 'Заказ доставлен в пункт выдачи' && $newParams['status'] == 'Заказ доставлен в пункт выдачи') {
echo 'Отправка SMS:<br>
Ваш заказ №'.$orderId.' на сумму '.number_format($newParams['sum'], 0, ',', ' ').' руб. доставлен в пункт выдачи по адресу '.$newParams['address_delivery_short'].'<br>
<br>
';
}
});
Далее аналогичным образом в заказ на событие order_updated добавляем наблюдателя, но теперь им выступает уведомление о заказе – объект класса OrdersNotifier
//Добавляем для заказа наблюдателя, которым выступает объект
$notifier = new OrdersNotifier();
$order->addObserver('order_updated', $notifier);
В конце скрипта изменяем заказ, устанавливая ему статус Заказ доставлен в пункт выдачи
//Изменяем статус заказа
$order->update(array(
'status' => 'Заказ доставлен в пункт выдачи'
));
Как мы помним, после изменения заказа срабатывает событие order_updated и выполняются обработчики всех наблюдателей, добавленных для этого события. В нашем случае выполнятся два обработчика – отправка SMS и письма о приходе заказа в пункт выдачи.
Полный код скрипта test.php
<?
//Подключаем классы для работы с заказами
require_once(__DIR__.'/order.classes.php');
//Создаём заказ
$order = new Order(18954);
//Добавляем для заказ наблюдателя, которым выступает анонимная функция-обработчик
$order->addObserver('order_updated', function($orderId, $oldParams, $newParams){
if ($oldParams['status'] != 'Заказ доставлен в пункт выдачи' && $newParams['status'] == 'Заказ доставлен в пункт выдачи') {
echo 'Отправка SMS:<br>
Ваш заказ №'.$orderId.' на сумму '.number_format($newParams['sum'], 0, ',', ' ').' руб. доставлен в пункт выдачи по адресу '.$newParams['address_delivery_short'].'<br>
<br>
';
}
});
//Добавляем для заказа наблюдателя, которым выступает объект
$notifier = new OrdersNotifier();
$order->addObserver('order_updated', $notifier);
//Изменяем статус заказа
$order->update(array(
'status' => 'Заказ доставлен в пункт выдачи'
));
?>
Результат выполнения скрипта test.php
Отправка SMS:
Ваш заказ №18954 на сумму 4 500 руб. доставлен в пункт выдачи по адресу г. Иваново, ул. Соколовая, д. 1
Отправка письма:
Ваш заказ №18954 на сумму 4 500 руб. доставлен в пункт выдачи по адресу г. Иваново, ул. Соколовая, д. 1