Obiettivo del modello di osservatore: la classe di osservatori risponde ai cambiamenti dello stato della classe osservabile.
Problema. Ci sono diversi processi all'interno della classe osservabile che dovrebbero essere osservati. Questi eventi sono indipendenti e lo stato non è un valore singolo, ma piuttosto è un insieme di processi diversi.
Potrebbe essere una cattiva progettazione, la classe osservabile è troppo grande e dovrebbe essere divisa in più piccoli. Ma cosa succede se non possiamo refactoring di quella classe e dobbiamo osservarlo.
Domanda. Dovremmo avere un singolo osservatore per tutte le modifiche di stato (processi, eventi). O dovremmo creare un osservatore per ogni cambiamento di stato (processo, evento).
O magari applicare l'osservatore è una cattiva idea e dovrebbe essere usato un altro modello di design.
Esempio
L'esempio potrebbe sembrare artificiale per il modello di osservatore, ma dovrebbe essere visto come un esempio minimo.
Considera il modulo di ricerca dove l'utente può cercare:
- autore
- titolo
- autore e titolo
Esiste la classe Reader
responsabile per ottenere i parametri.
C'è un altro compito: memorizzare le parole che gli utenti stanno cercando nel file (logger semplificato). Vogliamo memorizzare i valori dell'autore in un file e i valori del titolo nell'altro.
Non vogliamo aggiungere responsabilità per la classe Reader
né estenderlo. Vogliamo osservare il suo stato interiore.
Una modifica di stato (evento) si verifica quando il parametro dell'autore viene letto dal modulo.
Il secondo cambiamento di stato (evento) si verifica quando il parametro del titolo viene letto dal modulo.
Questi eventi sono indipendenti. Non ci interessa tutto lo stato Reader
(solo autore, solo titolo, titolo e autore). Siamo interessati solo agli stati dei lettori di parametri (autore letto o no) (titolo letto o no).
Un osservatore per tutti gli eventi
Quando viene notificato l'osservatore, non sa quale evento è successo. Viene introdotta la proprietà aggiuntiva whichState
. L'osservatore deve controllare il suo valore ( SEARCH_BY_AUTHOR
o SEARCH_BY_TITLE
) nel metodo update()
.
Codice:
<?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"));
?>
Due osservatori, ciascuno per un evento
Gli osservatori sono duplicati all'interno della classe osservabile. C'è una collezione authorObservers
e l'altra titleObservers
. Esistono metodi attachAuthorObserver()
, detachAuthorObserver()
, notifyAuthorObservers()
e ci sono metodi attachTitleObserver()
, detachTitleObserver()
, notifyTitlerObservers()
.
Codice
<?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"));
Aggiornamento: soluzione
Soluzione specifica basata sulla risposta di Robert Harvey.
Codice
<?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"));
?>