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.
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()
.
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.
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"));
?>