Pattern Observer — «Наблюдатель»

Observer (Наблюдатель) – это поведенческий шаблон проектирования, не менее популярный, чем упоминавшиеся ранее Registry или Singleton. Все, кто связаны с программирование пользовательского интерфейса, должны быть хорошо знакомы с данным шаблоном.

Чтобы определить область применения паттерна Observer, давайте рассмотрим часть базового функционала любой системы управления сайтом (CMS). Возьмем, например, возможность комментирования пользователями публикаций. Нередко, автор комментария может подписаться на все последующие комментарии или только на те, что являются ответом к его собственному. Также, скорее всего, о новых комментариях необходимо отправлять уведомления администратору и модераторам, вести лог и так далее. Зачастую это выглядит примерно так:

if ($commentSaveStatus) {
    // Send norifications
    sendNotifications();

    // Log data of the comment author 
    logAuthorData();

    // ...
    doSomethingElse();
}




Предположим, появилась необходимость подключить проверку с помощью сервиса Akismet. При организации, описанной выше, нам потребуется изменить код, добавить работу с Akismet API сразу после вызова doSomethingElse(). Если вы решите отказаться от использования Akismet, снова придется вносить правки в код.

Реализация паттерна Observer представляет собой связь «один-ко-многим» и реализуется с помощью нескольких классов. Один из них – Субъект. Класс Субъект информирует другие классы, которые называются Наблюдателями, о каких-либо событиях, произошедших внутри себя.

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

Я предлагаю реализацию шаблона Observer на языке PHP, но вы можете относиться к нему, как псевдокоду, если не знаете данный язык.

UML диаграмма классов, реализация которых представлена ниже:

observer

Код интерфейса Субъекта:

interface Observable
{
    public function attach(Observer $instance);

    public function detach(Observer $instance);

    public function notify();
}

Код интерфейса Наблюдателя:

interface Observer
{
    public function update(Observable $instance);
}

В целом, в этом заключается весь шаблон проектирования Observer. Ниже я привожу код классов, которые реализуют описанные интерфейсы.

Класс Субъект:

class Comment implements Observable
{
    private $status = 0;

    const SAVED_SUCCESS = 1;

    private $observers = array();

    public function getStatus() {
        return $this->status;
    }

    public function attach(Observer $instance) {
        foreach ($this->observers as $observer) {
            if ($instance === $observer) {
                return false;
            }
        }

        $this->observers[] = $instance;
    }

    public function detach(Observer $instance) {
        foreach ($this->observers as $key => $observer) {
            if ($instance === $observer) {
                unset($this->observers[$key]);
            }
        }
    }

    public function notify()  {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function save() {
        if (1) {
            $this->status = self::SAVED_SUCCESS;
        }

        $this->notify();
    }
}

Метод Comment::save() якобы выполняет сохранение комментария. Если бы это был реальный класс, то if(1) имело бы вид, например, if($saveStatus).

Методы Comment::detach() и Comment::attach() позволяют добавлять наблюдателей в стэк (массив) и удалять их оттуда в случае необходимости. Метод Comment:: notify() запускает цикл обхода Наблюдателей по списку и отправки уведомления каждому из них с помощью публичного метода Observer::update().

Код классов Наблюдателей, в которых реализован метод Observer::update():

class Logger implements Observer
{
    private function log($message) {
        echo __CLASS__ . ' : ' . $message;
    }

    public function update(Observable $subject) {
        if ($subject->getStatus() == Comment::SAVED_SUCCESS) {
            $this->log("Comment saved successfully\n", Comment::SAVED_SUCCESS);
        }
    }
}

class Mailer implements Observer
{
    private function send($message) {
        echo __CLASS__ . ' : ' . $message;
    }

    public function update(Observable $subject) {
        if ($subject->getStatus() == Comment::SAVED_SUCCESS) {
            $this->send("Comment saved successfully\n", Comment::SAVED_SUCCESS);
        }
    }
}

Оба класса предельно просты и идентичны. Можно было обойтись и одним, но я подумал, что для наглядности реализации паттерна Observer лучше использовать несколько Наблюдателей.

И еще один небольшой кусок кода, где весь механизм приводится в действие:

$comment = new Comment();
$comment->attach(new Logger);
$comment->attach(new Mailer);
$comment->save();

В результате выполнения будет:

Logger : Comment saved successfuly
Mailer : Comment saved successfuly

Стандартная библиотека PHP содержит встроенные интерфейсы (SplObserver и SplSubject), которые ничем не отличаются от тех, что я описал выше. Иными словами, если вам нужна именно такая реализация паттерна Observer, то лучше использовать SPL. Если необходимы какие-то отступления, потребуется собственная реализация.

Pattern Observer source code

Комментарии (4)

  1. Алексей

    Несколько спорный подход с сохранением комментарием самого себя, это чужая ответственность. Для сохранения, должен использоваться отдельный класс/объект, а в этом случае реализация становится не очевидной. Т.к. если навесить update на изменение свойства сущности — логирование или уведомление произойдёт раньше записи в БД, а если навешивать слушателя на хранилище — то у него появляется не свойственная ему ответсвенность, к тому же как-то нужно дополнительно определять какие слушатели должны выполняться.
    Есть мысли по этому поводу? Возможно есть пример реального использования паттерна?

    • Не рассматривайте приведенные примеры в рамках MVC, например. Здесь не идет речь о разделение на контроллеры и модели, а описывается сама суть Observer. Более того, паттерн можно внедрить в приложение на flat php, где никакой архитектуры в том виде, в котором вы ее ожидаете, не будет.

      Тем не менее, даже если вернуться к примерам из поста, то чем класс Comment не контроллер? Обязанностей модели я в нем не вижу. Метод Comment::save() может быть просто оберткой вызова модели. Реализация метода, как и всего класса, опущена и может подразумевать все необходимое.

  2. Во первых в качестве $observers можно использовать объект SplObjectStorage, во вторых зачем такие странные проверки через циклы foreach когда можно было бы просто использовать in_array()

  3. Константин

    По моему странновата запись вида
    public function update(Observable $subject) {
    if ($subject->getStatus() == Comment::SAVED_SUCCESS)

    Ведь getStatus() не является частью интерфейса Observable и почему жесткая привязка к Comment::SAVED_SUCCESS Если это не Comment будет.

    Нужно тогда наследовать от абстрактного класса например. и там реализовывать getStatus. Ну или другие варианты но не хардкод ))))

Добавить комментарий для Алексей Отменить ответ

Ваш e-mail не будет опубликован. Обязательные поля помечены *