Чем полезен SplSubject/SplObserver?

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

class MySubject implements SplSubject {
    protected $_observers = [];

    public function attach(SplObserver $observer) {
        $id = spl_object_hash($observer);
        $this->_observers[$id] = $observer;
    }

    public function detach(SplObserver $observer) {
        $id = spl_object_hash($observer);

        if (isset($this->_observers[$id])) {
            unset($this->_observers[$id]);
        }
    }

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

class MyObserver implements SplObserver {
    public function update(SplSubject $subject) {
        // something happened with $subject, but what
        // was it???
    }
}

$subject = new MySubject();
$observer = new MyObserver();

$subject->attach($observer);
$subject->notify();

Кажется, что эти интерфейсы практически бесполезны для любой реальной проблемы. Может ли кто-нибудь просветить меня?


Изменить:

Вот моя самая большая проблема с интерфейсом (хотя есть и другие):

public function update(SplSubject $subject, Event $event) { /* ... */ }

... выдает следующую фатальную ошибку:

PHP Fatal error:  Declaration of MyObserver::update() must be compatible with SplObserver::update(SplSubject $SplSubject)

Изменить №2:

Если сделать дополнительные параметры необязательными, установив для них значения по умолчанию, это предотвратит фатальную ошибку и предоставит способ передачи контекста, что делает реализации целесообразными. Раньше я не знал об этом, так что это в значительной степени отвечает на мой вопрос. Решение состоит в том, чтобы передать свои собственные данные события/сообщения и проверить их существование внутри SplObserver::update().


person FtDRbwLXw6    schedule 08.12.2012    source источник
comment
Я всегда чувствовал, что реализация этого учебника была хромой. Наблюдатель должен опросить субъект на наличие измененных данных, но ему нужно, черт возьми, использовать instanceof, прежде чем он сможет безопасно это сделать. Однако обратите внимание, что следующее удовлетворяет сигнатуре метода update(SplSubject $subject, $event = null) и очень помогает в ситуации.   -  person goat    schedule 12.12.2012
comment
@rambocoder: Да, это предотвращает фатальную ошибку, но открывает код для ошибок времени выполнения, если вы специально не проверите isset($event) (что не является большой проблемой, но как бы ни был прост шаблон Observer, я мог бы переписать эти интерфейсы в пару минут и избежать всех этих проблем).   -  person FtDRbwLXw6    schedule 12.12.2012
comment
Ну, вам нужно проверить нулевое значение в любом случае. Большим табу является то, что вам нужно использовать instanceof, потому что подсказка типа splsubject не гарантирует вам каких-либо методов, специфичных для объекта.   -  person goat    schedule 12.12.2012
comment
@rambocoder: Если бы интерфейс позволял мне делать public function update(SplSubject $subject, Event $event) (без значения по умолчанию), тогда проверка на нуль не требовалась бы, но это, очевидно, невозможно из-за несоответствия подписи. Тем не менее, предоставление добавленному параметру значения по умолчанию решает эту проблему, что было моей первоначальной проблемой. Если вы сделаете это ответом, я приму его.   -  person FtDRbwLXw6    schedule 12.12.2012
comment
ах ты прав. Я забыл, что php выбрасывает, когда вы передаете null (слишком много Java для меня в последнее время).   -  person goat    schedule 12.12.2012
comment
@drrcknlsn, рассмотрите возможность самостоятельного ответа на награду. Теперь, когда мы знаем, как обеспечить контекст для событий, все стало намного интереснее. Это также было моей основной жалобой на интерфейсы SPL. Я все еще ищу отличный, практичный пример из реального мира. Все, что дано до сих пор, является просто одноразовым демонстрационным кодом, за исключением одной ссылки на Zend DevZone, и я не уверен, что это считается.   -  person Charles    schedule 12.12.2012
comment
@Charles Я быстро просмотрел источник SplObserver и SplSubject (PHP 5.4) в надежде, что они были реализованы каким-то другим классом SPL. Похоже, изначально предполагалось использовать их с SplObjectStorage, у которого есть как методы присоединения, так и отсоединения, но нет уведомлений. Однако оба этих интерфейса не используются и не содержат никакой функциональности. Хотя они могут помочь реализовать шаблон, намекая на методы, которые можно использовать, в остальном они практически бесполезны.   -  person Leigh    schedule 12.12.2012
comment
Вы можете присвоить значения неопределенным свойствам в php. Почему бы просто не установить $this->event_name в методе уведомления перед вызовом update?   -  person Anthony    schedule 21.01.2015


Ответы (6)


Вы можете реализовать метод обновления с необязательным параметром и по-прежнему удовлетворять интерфейсу SplSubject.

class MyObserver implements SplObserver {
    public function update(SplSubject $subject, $eventData = null) {
        if (is_null($eventData))
            // carefull
    }
}
person goat    schedule 12.12.2012

It seems like these interfaces are pretty much useless for any real world problem. Can someone enlighten me?

Интерфейс

В то время как абстрактные классы позволяют вам обеспечить некоторую меру реализации, интерфейсы — это чистые шаблоны. interface может только define functionality; он никогда не сможет реализовать это. Интерфейс объявляется с помощью ключевого слова interface. Он может содержать объявления свойств и методов, но не тела методов.

Пример использования интерфейса

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

By itself, interfaces are not very useful потому что вы не можете создавать экземпляры интерфейсов, но интерфейсы играют важную роль в реализации методологий объектно-ориентированного проектирования which in real sense makes your live easier as a programmer потому что главным стимулом для объектно-ориентированного программирования является инкапсуляция (Вас не волнует, как реализована возможность. Вы, как программист, доступны только интерфейсу. Это также хороший способ следить за архитектурой системы)

SplSubject и SplObserver

Ортогональность — это добродетель. Одной из целей программистов должно быть создание компонентов, которые можно изменять или перемещать с минимальным воздействием на другие компоненты.

Если каждое изменение, которое вы вносите в один компонент, влечет за собой череду изменений в другом месте кодовой базы, задача разработки может быстро превратиться в спираль создания и устранения ошибок.

В SplSubject и SplObserver, потому что они оба interface to implement the Observer Design Pattern.

Шаблон Observer

Шаблон наблюдателя — это шаблон проектирования программного обеспечения, в котором объект, называемый субъектом, ведет список своих зависимых объектов, называемых наблюдателями, и автоматически уведомляет их о любых изменениях состояния, обычно вызывая один из их методов. Он в основном используется для реализации распределенных систем обработки событий.

  • Шаблон Observer определяет зависимость «один ко многим» между объектом-субъектом и любым количеством объектов-наблюдателей, так что при изменении состояния объекта-субъекта все его объекты-наблюдатели уведомляются и обновляются автоматически.
  • Паттерн Observer по существу позволяет неограниченному количеству объектов наблюдать или прослушивать события в наблюдаемом объекте (или субъекте), регистрируя себя. После того, как наблюдатели зарегистрированы на событие, субъект уведомит их, когда событие будет запущено.
  • Субъект обрабатывает это, сохраняя коллекцию наблюдателей и перебирая ее, когда происходит событие, чтобы уведомить каждого наблюдателя.
  • Шаблон наблюдателя регистрирует наблюдателей с субъектом.
  • У вас может быть несколько наблюдателей. Субъект должен вести список зарегистрированных наблюдателей, и когда происходит событие, он запускает (уведомляет) всех зарегистрированных наблюдателей.
  • Отменить регистрацию также возможно, когда нам не нужен ни один наблюдатель.

Пример 1. Система уведомления о процентной ставке по кредиту

$loan = new Loan("Mortage", "Citi Bank", 20.5);
$loan->attach(new Online());
$loan->attach(new SMS());
$loan->attach(new Email());

echo "<pre>";
$loan->setIntrest(17.5);

Вывод

Online    : Post online about modified Intrest rate of : 17.50
Send SMS  : Send SMS to premium subscribers : 17.50
Send Email: Notify mailing list : 17.50

Пример 2. Простой монитор регистрации пользователей

$users = new Users();

new Audit($users);
new Logger($users);
new Security($users);

$users->addUser("John");
$users->addUser("Smith");
$users->addUser("Admin");

Вывод

Audit    : Notify Audit about John
Log      : User John Create at Wed, 12 Dec 12 12:36:46 +0100
Audit    : Notify Audit about Smith
Log      : User Smith Create at Wed, 12 Dec 12 12:36:46 +0100
Audit    : Notify Audit about Admin
Log      : User Admin Create at Wed, 12 Dec 12 12:36:46 +0100
Security : Alert trying to create Admin

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

Недостаток шаблона проектирования Observer:

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

Общие классы

abstract class Observable implements SplSubject {
    protected $_observers = [];

    public function attach(SplObserver $observer) {
        $id = spl_object_hash($observer);
        $this->_observers[$id] = $observer;
    }

    public function detach(SplObserver $observer) {
        $id = spl_object_hash($observer);

        if (isset($this->_observers[$id])) {
            unset($this->_observers[$id]);
        }
    }

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



abstract class Observer implements SplObserver {
    private $observer;

    function __construct(SplSubject $observer) {
        $this->observer = $observer;
        $this->observer->attach($this);
    }
}

Загрузить примеры классов

class Loan extends Observable {
    private $bank;
    private $intrest;
    private $name;

    function __construct($name, $bank, $intrest) {
        $this->name = $name;
        $this->bank = $bank;
        $this->intrest = $intrest;
    }

    function setIntrest($intrest) {
        $this->intrest = $intrest;
        $this->notify();
    }

    function getIntrest() {
        return $this->intrest;
    }
}

class Online implements SplObserver {

    public function update(SplSubject $loan) {
        printf("Online    : Post online about modified Intrest rate of : %0.2f\n",$loan->getIntrest());
    }
}

class SMS implements SplObserver {

    public function update(SplSubject $loan) {
        printf("Send SMS  : Send SMS to premium subscribers : %0.2f\n",$loan->getIntrest());
    }
}

class Email implements SplObserver {

    public function update(SplSubject $loan) {
        printf("Send Email: Notify mailing list : %0.2f\n",$loan->getIntrest());
    }
}

Классы примеров регистрации пользователей

class Users extends Observable {
    private $name;

    function addUser($name) {
        $this->name = $name;
        $this->notify();
    }

    function getName() {
        return $this->name;
    }
}
class Audit extends Observer {

    public function update(SplSubject $subject) {
        printf("Audit    : Notify Autify about %s\n", $subject->getName());
    }
}
class Logger extends Observer {

    public function update(SplSubject $subject) {
        printf("Log      : User %s Create at %s\n", $subject->getName(),date(DATE_RFC822));
    }
}
class Security extends Observer {
    public function update(SplSubject $subject) {
        if($subject->getName() == "Admin")
        {
            printf("Security : Alert trying to create Admin\n");
        }
    }
}
person Baba    schedule 12.12.2012
comment
Я думаю, что Observable действительно может быть полезной чертой. (Моя формулировка очень уклончива: я склонен относиться к чертам так, как будто их не существует.) - person Levi Morrison; 18.12.2012
comment
Я действительно так не думаю ... лучший ответ на Traits Vs. Интерфейс видел... см. stackoverflow.com/a/9205347/1226894 - person Baba; 18.12.2012
comment
Этот ответ не решает вопрос, но подчеркивает проблему. Методы обновления знают только, что $subject реализует SplSubject, и все же они вызывают $subject->getName(), который не определен интерфейсом. SplSubject не определяет способ получения информации от субъекта, поэтому он не может точно знать, какие методы он может вызывать для субъекта, если не проверяет тип экземпляра, упомянутый в исходном вопросе. - person Courtney Miles; 18.09.2013
comment
@Baba - +1 и спасибо за ответ, но здесь будет добавлен еще один комментарий, вы можете создавать экземпляры интерфейсов, но не в PHP, однако, если вы отметили если объект является экземпляром интерфейса, он вернет true, если класс объекта реализует этот интерфейс. - person mamdouh alramadan; 17.11.2013
comment
Шаблон наблюдателя чрезвычайно полезен, особенно в более сложных системах. Вы можете следить за различными состояниями вашей системы любым из способов, которые вы можете себе представить (логирование, электронная почта, смс, неактивное уведомление, если что-то не работает и т. д.). Однажды я использовал его в большом проекте электронной коммерции, и это было невероятно полезно для того, чтобы убедиться, что все работает надежно, поскольку я всегда мог видеть внутреннюю работу в постановке. В. удобный. - person John Hunt; 19.10.2016

Все очень просто: шаблон субъект/наблюдатель бесполезен для событийной системы.

Шаблон наблюдателя не позволяет сказать: «Эта вещь была обновлена ​​X». Вместо этого он просто говорит, что она была обновлена. На самом деле я создал класс гибкого посредника, который можно использовать для система событий. В зависимости от ваших потребностей может быть полезен более жесткий API, но вы можете использовать его в качестве вдохновения.

Итак, когда модель субъект/наблюдатель полезна?

Это довольно распространенный шаблон при обновлении графического интерфейса, поскольку какой-то объект изменился. На самом деле ему не нужно знать, что его изменило или почему, просто его нужно обновить. Природа HTTP на самом деле не подходит для этого конкретного шаблона, потому что ваш PHP-код не привязан напрямую к HTML. Вы должны сделать новый запрос, чтобы обновить его.

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

person Levi Morrison    schedule 16.12.2012
comment
Угадайте, кто снова наткнулся на ваш ответ. Просто прочитайте этот код. Почему бы не использовать для этого \SplObjectStorage? - person kaiser; 08.09.2015
comment
@kaiser Используйте SplObjectStorage для чего именно? Вы имеете в виду класс посредников? - person Levi Morrison; 09.09.2015
comment
Да, для класса посредника. - person kaiser; 09.09.2015
comment
SplObjectStorage позволяет использовать только объекты в качестве ключей — класс-посредник использует строковые ключи. Я полагаю, что вы могли бы переключиться, но я не вижу смысла. - person Levi Morrison; 09.09.2015

К этим двум интерфейсам не прикреплена магическая функциональность, поэтому их реализация ничего не дает. Они действительно используются только для справочных целей. Существуют и другие внутренние интерфейсы PHP, такие как SeekableIterator. К методу seek не прилагается волшебная функциональность, и вам придется реализовать ее самостоятельно.

Есть некоторые внутренние интерфейсы PHP, такие как Traversable, которые имеют специальную функциональность, но это не относится к SplSubject и SplObserver — это, по сути, просто предлагаемый интерфейс для реализации шаблона Observer.

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

interface Event extends SplSubject {
   public function getEventData();
}

class MyEvent implements Event {
   //MySubject implementation above
   public function getEventData() {
      return "this kind of event happened";
   }
}

Вы также можете полностью игнорировать интерфейс Event или просто использовать проверки instanceof (уродливые), чтобы увидеть, какой «Тема» передается методу.

Что касается реального примера, эта ссылка предоставляет один, хотя использование SplObserver/SplSubject не является строго обязательным; в конце концов, это всего лишь интерфейсы. По сути, у вас может быть ExceptionHandler предметный класс и несколько наблюдателей, например, Mailer. Вы можете использовать set_exception_handler(array($handler, 'notify'));, и любое возникающее исключение уведомляет всех наблюдателей (например, Mailer, которое отправляет электронное письмо об исключении, которое было перехвачено - вам нужно будет получить исключение из какого-либо другого метода/члена ExceptionHandler).

EDIT: из комментариев я вижу, что вы планируете использовать другой аргумент для update, чтобы передать событие как отдельный объект. Думаю, все в порядке, но я предлагаю просто не разделять концепции Субъекта и События и дать Субъекту возможность либо содержать данные события, либо быть самими данными события. Вам нужно будет проверить, что объект события, который вы получаете, не равен нулю.

person Explosion Pills    schedule 11.12.2012

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

Представьте, что у вас есть событие applicationStart, на котором вам нужно запустить 10 функций.

function applicationStart() {
   // Some other logic 
   fnCall1();
   fnCall2();
   fnCall3();
   fnCall4();
   fnCall5();
   fnCall6();
   fnCall7();
   fnCall8();
   fnCall9();
   fnCall10();
   // Some other logic 
}

Теперь представьте, что вам нужно протестировать эту функцию, чтобы вызвать зависимость от всех остальных 10 функций.

Если вы используете SplSubject/SplObserver:

function applicationStart() {
    // Logic
    $Subject->notify();
    // Logic
}

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

Плюс код выглядит чище, так как вы не загрязняете его бизнес-логикой, которая ему не принадлежит. И одно удобное место для добавления триггеров

person E_p    schedule 12.12.2012

Взгляните на https://github.com/thephpleague/event, он отлично справляется со своей задачей. Я думаю, что это лучший пакет на сегодняшний день для этих целей. тоже не вижу смысла

public function notify(/* without args */) {

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

class EmailList
{
    const EVENT_ADD_SUBSCRIBER = 'email_list.add_subscriber';
    public function __construct($name, $subscribers = [])
    {
        // do your stuff 
        $this->emitter = new Emitter(); 
    }


    /**
     * Adds event listeners to this list
     * @param $event
     * @param $listener
     */
     public function addListener($event, $listener)
     {
         $this->emitter->addListener($event, $listener);
     } 

    /**
     * Adds subscriber to the list
     * @param Subscriber $subscriber
     */
    public function addSubscriber(Subscriber $subscriber)
    {
        // do your stuff 
        $this->emitter->emit(static::EVENT_ADD_SUBSCRIBER, $subscriber);
    }
}

// then in your code
$emailList = new EmailList();
$emailList->addListener(EmailList::EVENT_ADD_SUBSCRIBER, function($eventName, $subscriber) {
});
person radzserg    schedule 17.03.2017