3

Observer pattern aim: the observer class responds to the changes of the observable class state.

Problem. There are different processes inside observable class that should be observed. Those events are independent and the state is not single value but rather itself is a set of different processes.

That might be bad design, the observable class is too big and should be split into smaller ones. But what if we cannot refactor that class and we have to observe it.

Question. Should we have single Observer for all state changes (processes, events). Or should we create one Observer for each state change (process, event).

Or maybe applying observer is the bad idea at all and another design pattern should bu used.

Example

The example might look as artificial for the observer pattern but should be viewed as minimal example.

Consider search form where user can look for:

  • author
  • title
  • both author and title

There is the Reader class responsible for getting the parameters.

There is additional task: store words that the users are looking for in the file (simplified logger). We want to store author values in one file and title values in the another. We do not want add any responsibilities for the Reader class nor extend it. We want observe its inner state.

One state change (event) happens when the author parameter is read from the form. Second state change (event) happens when the title parameter is read from the form. Those events are independent. We are not interested in whole Reader state (only author, only title, title and author). We are interested only in states of parameter readers (author read or not) (title read or not).

One observer for all events

When observer is notified, it does not know which event happened. The additional property whichState is introduced. The observer must check its value (SEARCH_BY_AUTHOR or SEARCH_BY_TITLE) in the update() method.

enter image description here

Code:

<?php
class Reader implements \SplSubject 
{
    private $observers;    
    public $author = null;
    public $title = null;    
    // additional property to determine which state is observed
    public $whichState = null;
    // states
    const SEARCH_BY_AUTHOR = 'author';
    const SEARCH_BY_TITLE  = 'title';

    public function __construct() 
    {
        $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);
        }
    }

    /* simulates reading from form */
    public function readParams(array $params)
    {
        if (array_key_exists('author', $params) === true) {
            $this->author     = $params['author'];
            $this->whichState = Reader::SEARCH_BY_AUTHOR;
            $this->notify();
        }
        if (array_key_exists('title', $params) === true) {
            $this->title      = $params['title'];
            $this->whichState = Reader::SEARCH_BY_TITLE;
            $this->notify();
        }
    }
}

class BothParamsObserver implements \SplObserver
{
    public function update(\SplSubject $subject) 
    {
        if ($subject->whichState === Reader::SEARCH_BY_AUTHOR) {
            $author = $subject->author . "\r\n";
            file_put_contents("authors.txt", $author, FILE_APPEND);
        }
        elseif ($subject->whichState === Reader::SEARCH_BY_TITLE) {
            $title  = $subject->title  . "\r\n";
            file_put_contents("titles.txt", $title, FILE_APPEND);
        }
    }
}

$reader = new Reader();
$bothParamsObserver = new BothParamsObserver();
$reader->attach($bothParamsObserver);

file_put_contents("authors.txt", "");
file_put_contents("titles.txt", "");
// simulate receiving forms 
$reader->readParams(array("title" => "PHP"));
$reader->readParams(array("author" => "Zandstra"));
$reader->readParams(array("title" => "PHP", "author" => "Zandstra"));
?>

Two observers, each for one event

The observers are duplicated inside the observable class. There is one collections authorObservers and the other titleObservers. There are methods attachAuthorObserver(), detachAuthorObserver(), notifyAuthorObservers() and there are methods attachTitleObserver(), detachTitleObserver(), notifyTitlerObservers().

enter image description here

Code

<?php
class Reader
{
    private $authorObservers;
    private $titleObservers;
    public $author = null;
    public $title = null;    

    public function __construct() 
    {
        $this->authorObservers = new \SplObjectStorage();
        $this->titleObservers  = new \SplObjectStorage();
    }

    public function attachAuthorObserver($observer) 
    {
        $this->authorObservers->attach($observer);
    }

    public function attachTitleObserver($observer) 
    {
        $this->titleObservers->attach($observer);
    }

    public function detachAuthorObserver($observer) 
    {
        $this->authorObservers->detach($observer);
    }

    public function detachTitleObserver($observer) 
    {
        $this->titleObservers->detach($observer);
    }

    public function notifyAuthorObservers() 
    {
        foreach ($this->authorObservers as $observer) {
            $observer->update($this);
        }
    }

    public function notifyTitleObservers() 
    {
        foreach ($this->titleObservers as $observer) {
            $observer->update($this);
        }
    }

    /* simulates reading from form */
    public function readParams(array $params)
    {
        if (array_key_exists('author', $params) === true) {
            $this->author     = $params['author'];
            $this->notifyAuthorObservers();
        }
        if (array_key_exists('title', $params) === true) {
            $this->title      = $params['title'];
            $this->notifyTitleObservers();
        }
    }
}

class AuthorObserver
{
    public function update($subject) 
    {
        $author = $subject->author . "\r\n";
        file_put_contents("authors.txt", $author, FILE_APPEND);
    }
}

class TitleObserver
{
    public function update($subject) 
    {
        $title  = $subject->title  . "\r\n";
        file_put_contents("titles.txt", $title, FILE_APPEND);
    }
}

$reader = new Reader();
$authorObserver = new AuthorObserver();
$titleObserver = new TitleObserver();
$reader->attachAuthorObserver($authorObserver);
$reader->attachTitleObserver($titleObserver);

file_put_contents("authors.txt", "");
file_put_contents("titles.txt", "");
// simulate receiving forms
$reader->readParams(array("title" => "PHP"));
$reader->readParams(array("author" => "Zandstra"));
$reader->readParams(array("title" => "PHP", "author" => "Zandstra"));

Update: solution

Specific solution based on Robert Harvey's answer.

enter image description here

Code

<?php
class Reader implements \SplSubject 
{
    private $observers;    
    public $author = null;
    public $title = null;    
    // additional property to determine which state is observed
    public $whichState = null;
    // states
    const SEARCH_BY_AUTHOR = 'author';
    const SEARCH_BY_TITLE  = 'title';

    public function __construct() 
    {
        $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);
        }
    }

    /* simulates reading from form */
    public function readParams(array $params)
    {
        if (array_key_exists('author', $params) === true) {
            $this->author     = $params['author'];
            $this->whichState = Reader::SEARCH_BY_AUTHOR;
            $this->notify();
        }
        if (array_key_exists('title', $params) === true) {
            $this->title      = $params['title'];
            $this->whichState = Reader::SEARCH_BY_TITLE;
            $this->notify();
        }
    }
}

class AuthorObserver implements \SplObserver
{
    public function update(\SplSubject $subject) 
    {
        if ($subject->whichState === Reader::SEARCH_BY_AUTHOR) {
            $author = $subject->author . "\r\n";
            file_put_contents("authors.txt", $author, FILE_APPEND);
        }
    }
}

class TitleObserver implements \SplObserver
{
    public function update(\SplSubject $subject) 
    {
        if ($subject->whichState === Reader::SEARCH_BY_TITLE) {
            $title  = $subject->title  . "\r\n";
            file_put_contents("titles.txt", $title, FILE_APPEND);
        }
    }
}


$reader = new Reader();
$authorObserver = new AuthorObserver();
$titleObserver = new TitleObserver();
$reader->attach($authorObserver);
$reader->attach($titleObserver);

file_put_contents("authors.txt", "");
file_put_contents("titles.txt", "");
// simulate receiving forms 
$reader->readParams(array("title" => "PHP"));
$reader->readParams(array("author" => "Zandstra"));
$reader->readParams(array("title" => "PHP", "author" => "Zandstra"));
?>

1 Answer 1

1

It should be two observers.

Your Reader object should not have observer-specific attach and detach methods. Rather, it should have two methods for registering and de-registering concrete observer objects in a single observer collection.

Observer Pattern illustration from Wikipedia

Wikipedia

You can still use Reader:WhichState in your concrete observers to determine whether or not each concrete observer should take any action.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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