6
\$\begingroup\$

Is the below a good solution for an observer to monitor multiple subjects. My only fear is that if an observer was monitoring a wide range of subjects then the update method could get quite large checking each instance to see what type it is but I can't think of another way to implement this:

Journalist Subject

    class Journalist implements SplSubject {

    protected $observers;
    protected $name;
    protected $story;

    public function __construct( $name ) {
        $this->name = $name;
        $this->observers = new SplObjectStorage();
    }

    public function attach( SplObserver $observer ) {
        $this->observers->attach( $observer );
    }

    public function detach( SplObserver $observer ) {
        $this->observers->detach( $observer );
    }

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

    public function addStory( $story ) {
        $this->story = $story;
        $this->notify();
    }

    public function getStory() {
        return $this->story;
    }

    public function addName( $name ) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }

}

Website Subject

    class Website implements SplSubject {

    protected $observers;
    protected $website;
    protected $webPage;

    public function __construct( $website ) {
        $this->website = $website;
        $this->observers = new SplObjectStorage();
    }

    public function attach( SplObserver $observer ) {
        $this->observers->attach( $observer );
    }

    public function detach( SplObserver $observer ) {
        $this->observers->detach( $observer );
    }

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

    public function addWebPage( $webPage ) {
        $this->webPage = $webPage;
        $this->notify();
    }

    public function getWebPage() {
        return $this->webPage;
    }

    public function addWebsite( $website ) {
        $this->website = $website;
    }

    public function getWebsite() {
        return $this->website;
    }

}

The Sun Observer

class TheSun implements SplObserver {

    public function update( SplSubject $observable ) {

        if( $observable instanceof Journalist )
            echo __CLASS__ . ' noticed that the journalist ' . $observable->getName() . ' said: ' . $observable->getStory() . '<br />';

        if( $observable instanceof Website )
            echo __CLASS__ . ' noticed that the website ' . $observable->getWebsite() . ' uploaded a page to: ' . $observable->getWebPage() . '<br />';

    }

}

The Mirror Observer

class TheMirror implements SplObserver {

    public function update( SplSubject $observable ) {

        if( $observable instanceof Journalist )
            echo __CLASS__ . ' noticed that the journalist ' . $observable->getName() . ' said: ' . $observable->getStory() . '<br />';

        if( $observable instanceof Website )
            echo __CLASS__ . ' noticed that the website ' . $observable->getWebsite() . ' uploaded a page to: ' . $observable->getWebPage() . '<br />';

    }

}

The Guardian Observer

class TheGuardian implements SplObserver {

    public function update( SplSubject $observable ) {

        if( $observable instanceof Journalist )
            echo __CLASS__ . ' noticed that the journalist ' . $observable->getName() . ' said: ' . $observable->getStory() . '<br />';

        if( $observable instanceof Website )
            echo __CLASS__ . ' noticed that the website ' . $observable->getWebsite() . ' uploaded a page to: ' . $observable->getWebPage() . '<br />';

    }

}

Code

$Journalist = new Journalist( 'John' );
$Website = new Website( 'Stack Overflow' );

$TheSun = new TheSun;
$TheMirror = new TheMirror;
$TheGuardian = new TheGuardian;

$Journalist->attach( $TheSun );
$Journalist->attach( $TheMirror );
$Journalist->attach( $TheGuardian );

$Website->attach( $TheSun );
$Website->attach( $TheMirror );
$Website->attach( $TheGuardian );

$Journalist->addStory( 'Knock, Knock' );
$Website->addWebPage( 'http://www.hello.com' );

$Journalist->detach( $TheSun );
$Journalist->detach( $TheMirror );
$Website->detach( $TheGuardian );

$Journalist->addStory( 'Whos There?' );
$Website->addWebPage( 'http://www.goodbye.com' );

The code should run exactly as is above. Any advice would be very welcome.

A second question I have is what if I wanted to notify the observers of more than one different action, for example some services might want to know in the example above if a Website or Journalists name has changed separately to if they have posted a new article, how could this be best accomplished?

\$\endgroup\$
7
\$\begingroup\$

Traits

First of all, we have to remove duplications caused by the implementation of the SplObject interface. We have here a great case for a trait: http://php.net/manual/en/language.oop5.traits.php.

Here is a refactoring using a trait (the rest is unchanged):

<?php

trait Subject
{

    private $observer = null;

    public function attach(SplObserver $observer)
    {
        $this->getObservers()->attach($observer);
    }

    public function detach(SplObserver $observer)
    {
        $this->getObservers()->detach($observer);
    }

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

    protected function getObserver()
    {
        if (is_null($this->observer)) $this->observer = new SplObjectStorage();
        return $this->observer;
    }
}


class Journalist implements SplSubject
{

    use Subject;

    protected $name;
    protected $story;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function addStory($story)
    {
        $this->story = $story;
        $this->notify();
    }

    public function getStory()
    {
        return $this->story;
    }

    public function addName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

}

Website Subject

class Website implements SplSubject
{

    use Subject;

    protected $website;
    protected $webPage;

    public function __construct($website)
    {
        $this->website = $website;
    }

    public function addWebPage($webPage)
    {
        $this->webPage = $webPage;
        $this->notify();
    }

    public function getWebPage()
    {
        return $this->webPage;
    }

    public function addWebsite($website)
    {
        $this->website = $website;
    }
}

Dedicated subclass

In order to increase the separation of concern and the reusability of your code, I advise you to create specific subclasses for your subjects:

class JournalistAsSubject extends Journalist implements SplSubject 
{
    use Subject;

    public function addStory($story)
    {
        parent::addStory($story);
        $this->notify();
    }
}

class Journalist
{
    protected $name;
    protected $story;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function addStory($story)
    {
        $this->story = $story;
    }

    public function getStory()
    {
        return $this->story;
    }

    public function addName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

}

The dispatcher

But there is a better way to separate concerns: the use of a dispatcher:

class EventDispatcher
{
    private $listeners = [];
    public function addListener($event_type, $listener)
    {
        if(!array_key_exists($event_type, $this->listeners)) $this->listener[$event_type] = [];
        $this->listeners[$event_type][] = $listener;
    }

    public function dispatch($event_type, $event)
    {
        if(!array_key_exists($event_type, $this->listeners)) throw new \InvalidArgumentException($event_type.' does not exists');
        foreach($this->listeners[$event_type] as $listener) {
            $listener->update($event);
        }
    }
}

class ArtclePublishedEvent
{
    public function __construct(Author $author, $title, $content)
    {
        $this->author = $author;
        $this->title = $title;
        $this->content = $content;
    }

    public function getAuthor(){ return $this->author; }
    public function getTitle(){ return $this->title; }
    public function getContent(){ return $this->content; }
}

So we have now very simple entities:

class Author
{
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName(){ return $this->name; }
}

class NewsRoom
{
    public function update($event)
    {
        return $event->author->name.' as written a new article called: "'.$event->title.'"';
    }
}

Note: in a real software, the article has to be a separate entity which is injected in the event object

And so we can use the whole system like this:

$dispatcher = new EventDispatcher();
$dispatcher->addListener('article.publish', new NewsRoom());

$event = new ArticlePublishedEvent(new Author('John'), 'a fairy tale', 'some very short content');
$dispatcher->dispatch('article.publish', $event);
\$\endgroup\$
  • \$\begingroup\$ Thanks lilobase for some great tips, especially in regards the Events Dispatcher. I always kind of disregard traits in my mind as it's been drilled in to me that they are just a glorified copy and paste, however I do believe they have a use here... \$\endgroup\$ – Peter Featherstone Apr 2 '15 at 9:27
  • \$\begingroup\$ I give you an insight to know when to use specific OOP construction in order to maintain what we called the orthogonality: en.wikipedia.org/wiki/Orthogonality#Computer_science. Between two of your entity: - is a = inheritance - belong to = aggregation - has a = composition - behave like = trait \$\endgroup\$ – lilobase Apr 2 '15 at 10:04
  • \$\begingroup\$ Based on the relation between two entities you can have: is a = inheritance, belong to = aggregation, has a = composition, behave like = trait. For example here your Author behaves like a subject (=trait) and an Article has an Author (=composition). Another example can be: an invoice line belong to an invoice (=aggregation). \$\endgroup\$ – lilobase Apr 2 '15 at 10:12
  • \$\begingroup\$ Hi lilo, thanks very much for the clarification. I also have one further question in regards the Events Dispatcher and MVC or frameworks, where should the listeners be added (is it in a global place or in controllers, models etc.?) \$\endgroup\$ – Peter Featherstone Apr 2 '15 at 12:45
  • \$\begingroup\$ In the service layer, a service between your model and the controller. \$\endgroup\$ – lilobase Apr 2 '15 at 12:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.