Beobachter

Problem

Wenn ein Objekt seinen Zustand ändert, sollen davon abhängige Objekte benachrichtigt werden.

Motivation

Eine Änderung an einem Objekt erfordert Änderungen an anderen Objekten, um die Konsistenz des Gesamtsystems zu erhalten. An Stelle einer engen Kopplung wird eine lose Kopplung der Objekte angestrebt, bei der die beteiligten Objekte unabhängig voneinander variiert werden können.

Lösung

Das unter Beobachtung stehende Objekt, Subject genannt, stellt die folgenden Methoden zur Verfügung, über die sich andere Objekte, Observer genannt, für die Benachrichtigung bei Zustandsänderungen an- und abmelden können:

Die Benachrichtigung der Beobachter-Objekte erfolgt durch den Aufruf der Methode notify() des Subject-Objektes. Diese Methode ruft auf jedem als Beobachter registrierten Objekt dessen update()-Methode auf, die von den Beobachter-Objekten bereitzustellen ist.

Abbildung 7.1 zeigt zwei Klassen Subject und Observer, die die beschriebene Funktionalität zur Verfügung stellen und als Basis für zwei konkrete Klassen, ConcreteSubject und ConcreteObserver, dienen.

Abbildung 7.1. Struktur des Beobachter-Musters

Struktur des Beobachter-Musters


Beispiel 7.1: Die abstrakte Klasse Subject

<?php
require_once 'Observer.php';
 
abstract class Subject {
  protected $observers = array();
 
  public function attach(Observer $observer) {
      $this->observers[] = $observer;
  }
 
  public function detach(Observer $observer) {
    for ($i = 0; $i < sizeof($this->observers); $i++) {
      if ($this->observers[$i] === $observer) {
        unset($this->observers[$i]);
      }
    }
  }
 
  protected function notify() {
    for ($i = 0; $i < sizeof($this->observers); $i++) {
      $this->observers[$i]->update();
    }
  }
 
  public abstract function getState();
}
?>


Beispiel 7.2: Die abstrakte Klasse Observer

<?php
require_once 'Subject.php';
 
abstract class Observer {
  protected $subject = NULL;
 
  public function attach(Subject $subject) {
    $this->subject = $subject;
    $this->subject->attach($this);
  }
 
  public function detach() {
    if ($this->subject !== NULL) {
      $this->subject->detach($this);
    }
  }
 
  public abstract function update();
}
?>


Beispiel 7.3: Die Klasse ConcreteSubject

<?php
require_once 'Subject.php';
 
class ConcreteSubject extends Subject {
  protected $state = NULL;
 
  public function getState() {
    return $this->state;
  }
 
  public function doSomething() {
    // ...
 
    $this->notify();
  }
}
?>


Beispiel 7.4: Die Klasse ConcreteObserver

<?php
require_once 'Observer.php';
 
class ConcreteObserver extends Observer {
  protected $state = NULL;
 
  public function __construct(Subject $subject) {
    $this->attach($subject);
  }
 
  public function update() {
    print 'ConcreteObserver::update()';
 
    $this->state = $this->subject->getState();
  }
}
?>


Beispiel 7.5: Verwendung von Subject und Observer

<?php
require_once 'ConcreteSubject.php';
require_once 'ConcreteObserver.php';
 
$subject  = new ConcreteSubject;
$observer = new ConcreteObserver($subject);
 
$subject->doSomething();
$observer->detach();
$subject->doSomething();
?>
ConcreteObserver::update()


Anwendungsbeispiele

Das Problem, das ursprünglich das Beobachter-Muster motivierte, war die Kommunikation zwischen dem Model-Objekt und dessen View-Objekten in einer Applikation, die dem Model-View-Controller-Prinzip (MVC) folgt. Hierbei wird eine Anwendung in die drei Schichten Datenmodell (Model), Darstellungsschicht (View) und Steuerungsschicht (Controller) unterteilt. Diese Schichten sind voneinander entkoppelt: Das Datenmodell kennt weder Darstellungsschicht noch Steuerungsschicht. Die Darstellungsschicht registriert sich als Beobachter des Datenmodells und stellt dieses dar. Die Steuerungsschicht steuert den Ablauf der Anwendung und nimmt Änderungen am Datenmodell über dessen Programmierschnittstelle vor.

Ein anderes Einsatzgebiet des Beobachter-Musters ist das Protokollieren von Abläufen. Für die Verfolgung, und damit für das Protokollieren, der Testausführung bietet PHPUnit die Schnittstelle PHPUnit2_Framework_TestListener an. Objekte von Klassen, die diese implementieren, können über eine entsprechende Methode (addListener($listener)) an ein Objekt der Klasse PHPUnit2_Framework_TestResult "angehängt" werden, um dieses zu beobachten und so die Testausführung zu protokollieren.

Beispiel 7.6: Eine Implementierung der Schnittstelle PHPUnit2_Framework_TestListener

<?php
require_once 'PHPUnit2/Framework/TestListener.php';
 
class SimpleTestListener
implements PHPUnit2_Framework_TestListener {
  public function
  addError(PHPUnit2_Framework_Test $test, Exception $e) {
    printf(
      'Bei der Ausführung des Testfalls "%s"' .
      " trat ein Fehler auf.\n",
      $test->getName()
    );
  }
 
  public function
  addFailure(PHPUnit2_Framework_Test $test,
             PHPUnit2_Framework_AssertionFailedError $e) {
    printf(
      "Der Testfall \"%s\" schlug fehl.\n",
      $test->getName()
    );
  }
 
  public function
  addIncompleteTest(PHPUnit2_Framework_Test $test,
                    Exception $e) {
    printf(
      "Der Testfall \"%s\" wurde nicht implementiert.\n",
      $test->getName()
    );
  }
 
  public function startTest(PHPUnit2_Framework_Test $test) {
    printf(
      "Ausführung des Testfalls \"%s\" wurde gestartet.\n",
      $test->getName()
    );
  }
 
  public function endTest(PHPUnit2_Framework_Test $test) {
    printf(
      "Ausführung des Testfalls \"%s\" wurde beendet.\n",
      $test->getName()
    );
  }
 
  public function
  startTestSuite(PHPUnit2_Framework_TestSuite $suite) {
  }
 
  public function
  endTestSuite(PHPUnit2_Framework_TestSuite $suite) {
  }
}
?>


In Beispiel 7.7 wird zunächst ein neues Objekt der Klasse PHPUnit2_Framework_TestResult erzeugt. Diesem wird im Anschluss ein Objekt der Klasse SimpleTestListener aus Beispiel 7.6 als Listener hinzugefügt. Schließlich wird das PHPUnit2_Framework_TestResult-Objekt genutzt, um den testDoSomething Testfall (Beispiel 4.7) auszuführen.

Beispiel 7.7: Ausführung eines Testfalls unter Beobachtung des SimpleTestListener

<?php
require_once 'PHPUnit2/Framework/TestResult.php';
 
require_once 'SampleTest.php';
require_once 'SimpleTestListener.php';
 
$test = new SampleTest('testDoSomething');
 
$result = new PHPUnit2_Framework_TestResult;
$result->addListener(new SimpleTestListener);
$result->run($test);
?>
Ausführung des Testfalls "testDoSomething" wurde gestartet.
Ausführung des Testfalls "testDoSomething" wurde beendet.