Наблюдатель (Observer) — шаблон проектирования

3 февраля 2020
Шаблон предлагает схему, в которой при изменений объекта другие объекты получают уведомления и могут выполнять различные обработки.

Широкое применение  шаблон получил в реализации событий, когда после наступления какого-то события нужно выполнить ряд действий. Например, после оформления заказа и наступления события «Заказ оформлен» необходимо отправить администрации сайта письмо о поступившем заказе.

ПРИМЕР ЗАДАЧИ: в момент прихода заказа в пункт выдачи нужно отправить покупателю 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

Содержит следующие классы:
  1. Order  – заказ;
  2. 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