Facebook
Twitter
Google+
Kommentare
22

PHP Entwurfsmuster: Observer

Nachdem ich es die letzte Zeit ein wenig schleifen lassen habe, was PHP im speziellen angeht, will ich mich heute mal wieder auf die Programmierung konzentrieren. Jetzt wo ich drüber nachdenke, ist es was allgemeines, was ich erklären will, ich es aber anhand von PHP erklären werde. Mann mann mann, wie kompliziert und dabei haben wir nicht mal angefangen.Wie auch immer, ich werde heute versuchen ein nettes Entwurfsmuster zu erläutern, dem Observer Pattern, was wohl im deutschen Beobachter-Muster heißt, aber den Namen vergessen wir schnell wieder.

Um die Motivation hoch zu halten mal schnell am Anfang die Idee, die hinter diesem Muster steckt. Prinzipiell geht es darum informiert zu werden, falls sich ein bestimmtes Objekt ändert, ohne dafür das Objekt anpassen zu müssen. Eigentlich ist es noch besser, das Objekt muss gar nicht wissen, dass es beobachtet wird. Wir müssen lediglich definieren über welche Änderungen wir die Aussenwelt informieren müssen und dann kann es schon losgehen. So jetzt noch ein Beispiel aus der Praxis, falls mir eins einfällt. Das doofe „Auto informiert bei über 120 km/h einen Piepser“-Beispiel kann man ja fast gar nicht mehr bringen. Und aus dem Grund nehmen wir es einfach … nur ein Witz. Stellen wir uns ein digitales Hühnchen im Ofen vor. Der Ofen will informiert werden, wenn das Hähnchen 150 Grad in der Mitte erreicht hat, um den Koch zu informieren (habe ich erwähnt, dass ich Hunger habe?). Ich werde glaube ich in die Geschichte eingehen, als der PHP Entwickler mit den miesesten Beispielen. Naja wenigstens ein neuer Superlativ.

So genug gelabert, jetzt wollen wir den Code sprechen lassen. Fangen wir also an:

class Chicken
{
}

class Oven
{
}

Wir fangen wirklich mit dem simplen Grundgerüst an. Zwei Klassen und kein „Schi-Schi“. Damit können wir zwar noch nichts anfangen, aber für den Anfang reicht es. Jetzt brauchen wir die Möglichkeit einen Observer dem Chicken hinzuzufügen. Das geht ganz einfach über die addObserver Methode, die wir jetzt schreiben werden.

class Chicken
{
  private $observers = array( );

  public function addObserver( Observer $observer )
  {
    $this->observers[] = $observer;
  }
}

So, der Observer ist nun ganz einfach hinzugefügt. Der nächste Schritt ist das reagieren auf eine Veränderung. Wir müssen also alle Observer informieren, dass zum Beispiel die Temperatur des Gockels gestiegen ist. Das machen wir wie folgt:

class Chicken
{
  private $observers = array( );
  private $innerTemp = 30;

  const OBSERVER_TEMPERATURE_INCREASED = 'ob_temp_inc';

  public function getInnerTemperature( )
  {
    return $this->innerTemp;
  }

  public function addObserver( Observer $observer )
  {
    $this->observers[] = $observer;
  }

  public function increaseTemperature( $deltaTemp )
  {
    $this->innerTemp += $deltaTemp;
    $this->notifyObservers( self::OBSERVER_TEMPERATURE_INCREASED );
  }

  private function notifyObservers( $notificationType )
  {
    foreach( $this->observers as $observer ) {
      $observer->notify( $notificationType, $this );
    }
  }
}

Dadaaa, fertig. Unser Observer wird jetzt informiert, wenn die Temperatur erhöht wird. Und nicht nur unseren Observer, sondern alle, die wir hinzugefügt haben. $this geben wir auch noch mit, damit unser Observer auch was zum arbeiten hat. Unser konkreter Hühnchenfertig-Observer würde dann so ungefähr aussehen:

class ChickenObserver extends Observer
{
  public function notify( $notificationType, $chicken )
  {
    if ( $chicken->getInnerTemparature( ) > 150 ) {
      Alarm::ring( );
    }
  }
}

Eigentlich gibt es jetzt nichts mehr zu tun. Wie man den Observer dem Hühnchen hinzufügt, muss ich euch ja nicht mehr erklären. Vielleicht ist euch ja aufgefallen, dass das Hühnchen nicht weiss, dass es einen Ofen oder ähnliches gibt. Die Klasse muss sich also nur um das kümmern, was wirklich zu ihr gehört. Ich baue mir kein God-Objekt, das alles mögliche macht und auch bei jedem neuen Use-Case erweitert wird. Separation of Concerns ist hier auch wieder das Zauberwort. Wunderbar saubere Klassen sind die Folge und was will ein OOP Jünger lieber.

Die PHP Macher haben sich aber auch schon Gedanken über dieses Muster gemacht und es ging meiner Meinung nach an einer Stelle ziemlich in die Hose. In der SPL wird ein Interface für alles „Observable“ definiert. Um einen Observer hinzuzufügen muss man hier die Methode „attach“ verwenden. attachObserver wäre aber der eindeutig bessere Name, denn so kann ich die Methode attach gar nicht mehr verwenden, den Namen habe ich mir jetzt auf immer aus meinem Sortiment verbannt. Dabei ist attach so allgemein, dass ich es nicht ausschließen kann, dieses zu brauchen. Naja egal, ich hatte mich glaube ich schon mal darüber ausgelassen.

Ich hoffe, dass ich euch ein wenig die Eleganz des Entwurfsmuster ein wenig näher bringen konnte. Ich jedenfalls verwende es häufig und es hilft einen ungemein seine Klassen sauber zu halten und OOP strkt durchzuhalten.

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

22 Comments

  1. Leider konntest du mir so gar nicht die Eleganz des Entwurfmusters näherbringen.

    In Java verstehe ich es ja noch, dass es observer gibt. Da zur Laufzeit einfach ein anderes Objekt notified wird. Das ist auch schön, gut und einigermaßen elegant.

    Nur in PHP habe ich das noch nicht so richtig verstanden, da es ja ein einziges Skript ist welches nur runtergearbeitet wird. Wozu braucht man da einen Observer der etwas überwacht wenn sich der Status nicht wirklich zur Laufzeit ändern kann?

    Ich habe mich mit dem Observerpattern in PHP zwar noch nicht sehr intensiv beschäftigt, muss ich dazu sagen.

    Reply
  2. Aber nur weil ein Skript von oben nach unten durchläuft, heißt es doch noch lange nicht, dass man keine Objekte zu überwachen hat. Objekte können genau so persistent, dank Datenbank und Cache, über mehrere Aufrufe bleiben. Ein Seitencounter, der z.B. bei jedem 1000 Abruf Lufballons in die Luft steigen lassen soll, wäre so ein Beispiel.
    Objekte können also ohne Probleme den Status ändern, da sehe ich keinen Unterschied zu Java. Nur das Java eben ein wenig anders die Persistenz herstellt.

    Reply
  3. > “ […] dem Observer Pattern, was wohl im deutschen Beobachter-Muster heißt, aber den Namen vergessen wir schnell wieder […]“
    Nils, soetwas aus deinem „Munde“ zu hören stimmt mich glücklich 🙂

    Zur Notwendigkeit dieses Patterns in PHP:
    Java hat das Observer-Pattern durch seine intensive Anwendung in AWT&Swing sehr populär gemacht. Das ist gut! Trotzdem sollte man sich vor Augen halten, dass das Pattern weit mehr Anwendungszwecke hat als nur UI-Notifications. Wie Nils schon geschrieben hatte, dient es der Seperation of Concerns. Klassen, die von Ereignissen anderer Klassen abhängig sein müssen, können es nun erreichen, dass keine direkte Abhängigkeit (im Sinne des Codes) zwischen den Klassen entsteht.

    Das muss auch nicht mal auf persistente Objekte beschränkt sein:

    Stell dir vor, du hast ein Command-Line PHP-Programm, welches 1.000 HTTP-Requests machen muss. Du möchtest sicherlich den Fortschritt auf der Console visualisieren.

    Schreibst du nun jedoch deine Systemausgaben (e.g. echo) direkt in den Quellcode der Bibliothek, welche die Übertragung macht? Naja, dann kannst du diese Bibliothek auf keinen Fall für einen anderen Zweck verwenden. (E.g. beim Generieren einer Website)

    Also musst du den Code der HTTP-Komponente vom Command-Line-Code trennen.

    class HTTP
    {
        function addObserver( $observer ){...}
        function send()
        { 
          for( ... $this->observers ... ) {
              $obs->notify( 'httpEvent', $this );
          }
        }
    }
    class ConsoleProgram
    {
        function notify( $evt, $obj ) 
        {
          echo "HTTP Request to " . $obj->GetXXX() . " started!" ;
        }
    }

    Logisch, oder?

    Grüße,
    Timo

    Reply
  4. Gute Beispiele sind wirklich schwer zu finden… Danke!

    Aber dieses Pattern ist so wichtig, da gibt es schon viele Anwendungen… Dein Hühnchen fand ich übrigens genial! Danke für die Erheiterung am Morgen 😀

    Reply
  5. „Vielleicht ist euch ja aufgefallen, dass das Hühnchen nicht weiss, dass es einen Ofen oder ähnliches gibt. “

    Spätestens, wenn das Hühnchen im Ofen ist, weiss es dass es so etwas gibt! ;D

    In deinem Observer hast du übrigens einen Fehler. Dort rufst du die $chicken Methode getTemparature() auf, richtig wäre aber getInnerTemperature()

    Reply
  6. Ein weiterer klitzekleiner Fehler ist ein Tippfehler in der foreach-Schleife von notifyObservers.
    Da iterierst du „$this->obsersvers“ obwohl es $this->observers heißen sollte. Ist nur ein klitzeklitzekleiner Fehler.

    Ich persönlich finde derzeit jedoch keine Anwendung für dieses Entwurfsmuster, ich bin und bleibe Fan von Singleton 🙂

    Reply
  7. @PHPDave: Vielen Dank, habe es behoben. Singleton ist auch ein schönes Muster, wenn man es verwenden kann. Schau dir mal dependency injetion an, vielleicht ist dies auch was für dich.

    Reply
  8. @PHPDave
    „Ich persönlich finde derzeit jedoch keine Anwendung für dieses Entwurfsmuster, ich bin und bleibe Fan von Singleton“

    Äh…man sucht nicht nach Anwendungen für Patterns, sondern nach Patterns, die ein konkretes Problem einer Anwendung lösen können.

    Bsp:
    Ich muss einen Nagel ins Holz schlagem – Ah, das Hammer Pattern!
    vs.
    Ich habe einen Hammer – was mach ich nu damit?

    Und Singletons gelten, zumindest oder mindestens wenn man testgetrieben entwickelt, als Anti-Pattern. Und das nicht zu Unrecht. Ist wohl so populär, weil es einfach zu verstehen ist und das obige Hammer-Problem von hinten rum angeht.
    Je komplexer die Dinge werden, desto mehr sieht man aber, dass das Pattern meist für die Katz ist und nicht wirklich hilft.

    Cheers!

    Reply
  9. Da ich gerade so durch das Web gegoogelt bin um Informationen zu dem Observer-Pattern zu bekommen habe ich diese alten Artikel gefunden.

    Was mir dabei aufgefallen ist was der Parameter „$notificationType“ in der Funktion „notify“ des Observers da zu suchen hat. Somit ist es dem Oberserver also möglich zu überprüfen von welcher Funktion er aufgerufen wurde.

    Ich finde das verfehlt doch irgendwie den Zweck des Patterns und kommt doch dann eher dem EventDispatching näher, oder?

    Ich würde es auch am liebsten so anwenden da ich genau vor so einen Problem stehe doch ich komm mir dabei so „dirty“ vor als wenn ich das Pattern zum EventDipatching misshandeln würde.

    Reply
  10. Noch ein vielleicht gängigeres Beispiel für das Observer-Pattern in PHP:

    Bei einer Webseite mit relativ hohen Zugriffszahlen wird man früher oder später gezwungen sein ein Caching zu implementieren und die HTML-Ausgabe für eine gewisse Zeit direkt auszuliefern, anstatt eine Seite jedes Mal neu zu erstellen. Gängiger Weg ist hier diese Ausgabe direkt als HTML mit einer gewissen Lebenszeit im Dateisystem abzulegen.

    Nehmen wir nun mal ein Forum an. Eine Forenseite ist so lange gültig, bis ein neuer Eintrag kommt, ein Eintrag geändert wird oder eine Antwort erscheint.

    Jetzt kann man natürlich das ganze von Hand zu Fuß fest im Code verdrahten und bei der Erstellung eines neuen Inhaltes alle zugehörigen Dateien direkt löschen. Das klappt ganz toll, bis sich die Struktur des Caches ändert, oder eine neue Art der Ausgabe der Inhalte hinzu kommt.

    Sinnvoller ist es da, das Caching auszulagern, womit man vor dem Problem steht, wie der Cache erfährt dass etwas geändert wurde und gleichzeitig den Aufwand gering hält, also nicht pauschal alle Dateien im Cache löschen muss.

    Hier bietet sich das Observer-Pattern an indem der Cache als Observer im Subject (den Beiträgen) registiert wird, denn nur der Cache weiß, was wo gecached wurde und wieviel gelöscht werden muss, wenn sich ein Inhalt ändert.

    Im Gegensatz zur Möglichkeit die Cache-Änderung im Script, welches einen neuen Beitrag annimmt anzustoßen bietet das Observer-Pattern hier eine viel größere Flexibilität, da es die Einführung neuer Arten von Inhalte aus der Verarbeitung dieser heraus hält.

    Reply
  11. Ich habe da eine kleine Frage… woher nimmst du die Klasse „Observer“? – Ist die schon inbuild? Ich perönlich würde dafür eine abstract class Observer machen. Sähe bei mir dann folgendermaßen aus:

    Das wäre doch prinzipiell richtig, oder?

    Reply
  12. Also nochmal…

    abstract class Observable {
    public function addObserver(Observer $observer);
    private function notifyObservers($notificationType);
    }
    abstract class Observer {
    public abstract function notify($notification, Observable $caller);
    }
    class ChickenObserver extends Observer {
    public function notify($notificationType, Observable $caller) {
    var_dump($notificationType);
    var_dump($caller);
    }
    }
    class Chicken implements Observable {
    private $observers = array( );
    private $innerTemp = 30;
    const OBSERVER_TEMPERATURE_INCREASED = 'ob_temp_inc';

    public function getInnerTemperature() {
    return $this->innerTemp;
    }

    public function addObserver(Observer $observer) {
    $this->observers[] = $observer;
    }

    public function increaseTemperature($deltaTemp) {
    $this->innerTemp += $deltaTemp; $this->notifyObservers(self::OBSERVER_TEMPERATURE_INCREASED);
    }

    private function notifyObservers($notificationType) {
    foreach($this->observers as $observer) {
    $observer->notify($notificationType, $this);
    }
    }
    }

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen