Das Observer Design Pattern

Studienprojekt von Philipp Hauer. 2009 - 2010. ©

Inhalt

Einführung

Gegeben sei folgendes Szenario: Der FAZ-Verlag versendet die jeweils aktuelle Zeitung an seine Abonnenten Familie Fischer und Meier. Bei jeder neuen Zeitung soll eine Auslieferung erfolgen.

Funktionsweise

Eine "Quick und Dirty"-Lösung würde die Klasse FAZVerlag modellieren, die eine Referenz auf einmal Familie Fischer und einmal Familie Meier hält. Beide Familienklassen besitzen eine Schnittstelle mit der der FAZVerlag seine Zeitung an sie übermitteln kann (erhalteZeitung(Zeitung)).

Einführung in das Observer Design Pattern: Schlechte Quick and Dirty Lösung

Bemerkung: Natürlich ist es weniger sinnvoll, für Familie Fischer und Meier zwei verschiedene Klassen zu erstellen. Klüger wäre es, einfach eine Klasse "Familie" mit einem Namen-Attribut ("Fischer", "Meier") zu erstellen. Aus didaktischen Gründen sei dieser Gedanke hier allerdings weiter verfolgt.Es geht darum, dass die Abonnenten von verschiedenen Klassen sind.

Code von FAZVerlag:

public class FAZVerlag { 

    private Zeitung aktuelleZeitung; 

    private FamilieFischer famFischer; 

    private FamilieMeier famMeier; 


    //Nach dem einen neue FAZAusgabe gesetzt wurde 
    public void verteileZeitung() { 
        famFischer.erhalteZeitung(aktuelleZeitung); 
        famMeier.erhalteZeitung(aktuelleZeitung); 
    } 
} 

Unschwer zu erkennen, dass dies eine suboptimale Lösung ist. Die Nachteile liegen auf der Hand:

  • Enge Kopplung zwischen FAZVerlag und seinen Abonnenten. Die Klasse FAZVerlag hat Referenzen auf die konkreten Implementierungen (famFischer und famMeier) und codiert die Methodenaufrufe auf diesen fest in seiner verteileZeitung()-Methode. Dadurch ergeben sich eine Reihe von praktischen Nachteilen:
  • Die Erweiterbarkeit ist stark eingeschränkt. Kommt ein neuer Abonnent hinzu, so muss der Code der FAZVerlag Klasse stark modifiziert werden (neuen Instanzvariable, geänderte verteileZeitung()). Unvorstellbar, wenn pro Tag 100 neue Abonnenten hinzukommen...
  • Es gibt keine Möglichkeit dynamische Abos zu implementieren. Die Abonnenten sind fest codiert. Zur Laufzeit können keine Änderungen (An- oder Abmelden) durchgeführt werden.

Es seien nun zwei fundamentale OO-Entwurfsprinzipien in Betracht gezogen:

Programmiere auf Schnittstellen, nie auf Implementierungen.

 Identifiziere jene Aspekte, die sich ändern und trenne sie von jenen, die konstant bleiben.

Was bleibt konstant? Der Aufruf von erhalteZeitung(Zeitung) auf jedem Abonnenten. Aus den Abonnenten lässt sich somit eine gemeinsame Schnittstelle abstrahieren: Ein Interface Abonnent mit der Methode erhalteZeitung(Zeitung).

Einführung in das Observer Design Pattern: Gekapselte Abonnenten

Und was sollte variabel sein? Die Abonnenten, die wir dynamisch hinzufügen und entfernen wollen. Dank der Kapselung unserer konkreten Abonnenten hinter der Abonnentschnittstelle ist es nun möglich, dass der FAZVerlag nur noch das Interface kennt. Er interessiert sich nicht dafür, wer seine Abonnenten wirklich sind. Er weißt nur, dass sie Abonnenten sind und damit die Methode erhalteZeitung(FAZAusgabe) implementieren. Durch diese Entkopplung der FAZVerlag von den konkreten Abonnenten kann FAZVerlag alle Abonnenten in einer simplen Liste vorhalten. Melden sich Abonnenten an (aboHinzufügen(Abonnent)) so werden sie der Liste hinzugefügt, melden sich welche ab (aboEntfernen(Abonnent)), so werden sie von der Liste entfernt. Soll eine neue Zeitung verteilt werden (verteileZeitung()), so muss lediglich über die Liste aller Abonnenten iteriert und auf jedem die erhalteZeitung(Zeitung)-Methode aufgerufen werden. Die Methode verteileZeitung() wird mit der zu verteilenden Zeitung parametrisiert.

Einführung in das Observer Design Pattern: Nicht kohärente Verlagsklasse

Quellcode FAZVerlag:

public class FAZVerlag{ 

    private List abonnentenList = new ArrayList(); 

    private Zeitung aktuelleZeitung; 


    public void aboHinzufuegen(Abonnent abonnent) { 
        abonnentenList.add(abonnent); 
    } 

    public void aboEntfernen(Abonnent abonnent) { 
        abonnentenList.remove(abonnent); 
    } 

    private void verteileZeitung(Zeitung zeitung) { 
        for (Abonnent abonnent : abonnentenList) { 
            abonnent.erhalteZeitung(zeitung); 
        } 
    } 

    public void setAktuelleZeitung(Zeitung aktuelleZeitung) { 
        this.aktuelleZeitung = aktuelleZeitung; 
        //Nach dem einen neue Zeitung gesetzt wurde, werden alle Abonnenten benachrichtigt. 
        verteileZeitung(aktuelleZeitung); 
    } 

    public Zeitung getAktuelleZeitung() { 
        return aktuelleZeitung; 
    } 
} 

Allerdings ist unsere FAZVerlag-Klasse noch nicht kohäsiv, da die Administrations- und Aktualisierungsmethoden mit den FAZspezifischen (getAktuelleZeitung(), setAktuelleZeitung()) vermischt sind. Diese Administrationsmethoden können für jeden möglichen Verlag genutzt werden, sind damit abstrahierbar und in eine abstrakte Superklasse auslagerbar. Somit können wir später einen neuen Verlag neben der FAZ modellieren und vorhanden Code wiederverwenden.

Einführung in das Observer Design Pattern: Kohärente Verlagsklassenhierarchie

Unser neues Klassendesign ist damit fertig gestellt. In der Zusammenschau ergibt sich folgendes Diagramm:

Einführung in das Observer Design Pattern: Fertiger Klassenentwurf

Quellcode Verlag, FAZVerlag:

public abstract class Verlag { 

    private List abonnentenList = new ArrayList(); 

    public void aboHinzufuegen(Abonnent abonnent) { 
        abonnentenList.add(abonnent); 
    } 

    public void aboEntfernen(Abonnent abonnent) { 
        abonnentenList.remove(abonnent); 
    } 

    protected void verteileZeitung(Zeitung zeitung) { 
        for (Abonnent abonnent : abonnentenList) { 
            abonnent.erhalteZeitung(zeitung); 
        } 
    } 
} 
public class FAZVerlag extends Verlag { 

    private Zeitung aktuelleZeitung; 

    public void setAktuelleZeitung(Zeitung aktuelleZeitung) { 
        this.aktuelleZeitung = aktuelleZeitung; 
        //Nach dem einen neue Zeitung gesetzt wurde, werden alle Abonnenten benachrichtigt. 
        verteileZeitung(aktuelleZeitung); 
    } 

    public Zeitung getAktuelleZeitung() { 
        return aktuelleZeitung; 
    } 
} 			
Quellcode Zeitung:

public class Zeitung { 

    //Ein examplarisches Field. 
    private final String titel; 

    public Zeitung(String titel) { 
        this.titel = titel; 
    } 

    public String getTitel() { 
        return titel; 
    } 
}  
Quellcode Abonnent mit den Realisierungen FamilieFischer, FamilieMeier und FirmaXY:

interface Abonnent { 

    public void erhalteZeitung(Zeitung zeitung); 

} 

class FamilieFischer implements Abonnent { 

    public void erhalteZeitung(Zeitung zeitung) { 
        System.out.println("Familie Fischer erhielt die aktuelle Zeitung: " + zeitung.getTitel()); 
    } 
} 

class FamilieMeier implements Abonnent { 

    public void erhalteZeitung(Zeitung zeitung) { 
        System.out.println("Familie Meier erhielt die aktuelle Zeitung: " + zeitung.getTitel()); 
    } 
} 

class FirmaXY implements Abonnent { 

    public void erhalteZeitung(Zeitung zeitung) { 
        System.out.println("Firma XY erhielt die aktuelle Zeitung: " + zeitung.getTitel()); 
    } 
}  			
Beispielclient:

public class Beispielclient { 

    public static void main(String[] args) { 
        FAZVerlag verlag = new FAZVerlag(); 
        verlag.aboHinzufuegen(new FamilieFischer()); 
        verlag.aboHinzufuegen(new FamilieMeier()); 
        FirmaXY firma = new FirmaXY(); 
        verlag.aboHinzufuegen(firma); 

        verlag.setAktuelleZeitung(new Zeitung("Skandal!")); 
        //Familie Fischer erhielt die aktuelle Zeitung: Skandal! 
        //Familie Meier erhielt die aktuelle Zeitung: Skandal! 
        //Firma XY erhielt die aktuelle Zeitung: Skandal! 

        verlag.aboEntfernen(firma); 
        verlag.setAktuelleZeitung(new Zeitung("Doch alles halb so wild!")); 
        //Familie Fischer erhielt die aktuelle Zeitung: Doch alles halb so wild! 
        //Familie Meier erhielt die aktuelle Zeitung: Doch alles halb so wild! 
    } 
} 			

Nun erfreuen wir uns der gewonnen Vorteile:

  • Verlag und Abonnenten sind entkoppelt, da der Verlag seine konkreten Abonnenten nicht kennt. Dies ermöglicht:
  • Problemloses Erstellen und Hinzufügen von neuen Abonnenten ohne den Verlagcode anfassen zu müssen. Der neue Abonnent muss einzig und allein das Abonnenteninterface implementieren. Wir erhalten hohe Wiederverwendbarkeit und Erweiterbarkeit.
  • Dynamik. Abonnenten können zur Design- und Laufzeit beliebig hinzugefügt und wieder entfernt werden.
  • Unabhängiges Wiederverwenden und Variieren von Verlag und Abonnenten. Eine Klasse kann nun Abonnent von vielen verschiedenen Dingen werden, wie Newsletter, Magazinen oder anderen Zeitungen. Es muss nur die entsprechenden Interfaces implementieren und sein Abo anmelden.

    Flexible Wiederverwendbarkeit von Abonnenten im Observer Pattern: Multiples Abonnieren

Es zeigt sich, dass durch den Einsatz des Observer Patterns, ein hohes Maß an Flexibilität und Dynamik gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.

Nach dieser Einführung wird im folgenden Abschnitt das Observer Design Pattern formalisiert, näher analysiert und diskutiert.

Das Zeitungsbeispiel mit Observer Pattern Termini

Klasse Observer Teilnehmer
FAZVerlag Subject (oder Observable)
Abonnent Observer (oder Dependents)
verteileZeitung(Zeitung) Methode mit der die Benachrichtigung aller Observer ausgelöst wird.
erhalteZeitung() Aktualisierungsmethode (oder Updatemethode, Benachrichtigungsmethode) der Observer

Im Zeitungsbeispiel wurde das sogenannte Push-Modell realisiert. Dazu später mehr.

Analyse und Diskussion

Gang Of Four-Definition

Observer:
"Definiere eine 1-zu-n-Abhängigkeit zwischen Objekten, so dass die Änderung des Zustands eines Objekts dazu führt, das alle abhängigen Objekte benachrichtigt und automatisch aktualisiert werden."
([GoF], Seite 287)

Beschreibung

Prinzip des Observer Design Patterns

Das Observer Pattern ermöglicht, dass sich Objekte (Observer, beobachtendes Objekt) bei einem anderem Objekt (Subject, beobachtetes Objekt) registrieren und fortan vom diesem informiert werden, sobald es sich ändert.

Für die Observer wird eine einheitliche Schnittstelle (Interface) mit mindestens einer Aktualisierungsmethode definiert. Diese wird vom Subject im Falle von Aktualisierungen aufgerufen und ist in den meisten Fällen mit näheren Daten zur Änderung parametrisiert. Konkrete Observer implementieren das Interface und damit die Aktualisierungsmethode und bestimmen somit, wie der Observer auf die Benachrichtigung reagieren soll.

Das Subject benötigt Administrationsmethoden, damit sich Observer an- und abmelden können. Meldet sich ein Observer an, so nimmt das Subject es in seine Liste der zu benachrichtigen Objekte auf. Treten nun Änderungen am Subjectzustand auf, so werden alle registrierten Observer informiert (notifyObservers()). Dies geschieht, in dem über die Observerliste des Subjects iteriert wird, und auf jedem Observer die Aktualisierungsmethode (update()) aufgerufen wird.

Das Observer Design Pattern

Subject:

public abstract class Subject { 

    private final List observerList = new ArrayList(); 

    public void register(Observer newObserver){ 
        observerList.add(newObserver); 
    } 

    public void unregister(Observer newObserver){ 
        observerList.remove(newObserver); 
    } 

    protected void notifyObservers(int state){ 
        for (Observer observer : observerList) { 
            observer.update(state); 
        } 
    } 
}  			
ConcreteSubject mit einem int als Zustand. Wenn dieser geändert wurde (setState()) werden alle Observer benachrichtigt:

public class ConcreteSubject extends Subject { 

    private int state; 

    public void setState(int state) { 
        this.state = state; 
        //Wenn das Subject die Aktualisierung selbst durchführen soll, 
        //alternativ kann die Methode auch vom Client aufgerufen werden. 
        notifyObservers(state); 
    } 

    public int getState() { 
        return state; 
    } 

} 			
Observer. Hier ist update() mit einem int parametrisiert. Der neue Zustand des Subject wird in dieser Form übergeben (push-Methode). ConcreteObserverA, ConcreteObserverB:

public interface Observer { 

    public void update(int state); 

} 

public class ConcreteObserverA implements Observer { 

    public void update(int state) { 
        System.out.println("Concrete Observer A is updated with "+state); 
        //ggf. Modifikationen mit setState(). 
    } 
} 
public class ConcreteObserverB implements Observer { 

    public void update(int state) { 
        System.out.println("Concrete Observer B is updated with "+state); 
        //ggf. Modifikationen mit setState(). 
    } 
} 			
Beispielclient:

public class Client { 

    public static void main(String[] args) { 
        ConcreteSubject concreteSubject = new ConcreteSubject(); 
        concreteSubject.register(new ConcreteObserverA()); 
        concreteSubject.register(new ConcreteObserverB()); 

        concreteSubject.setState(77); 
        //Concrete Observer A is updated with 77 
        //Concrete Observer B is updated with 77 
    } 
} 			

Variationen

Aufgrund der großen Beliebtheit des Observer Patterns haben sich viele Variationen ausgebildet. Dabei gibt es kein richtig und falsch, sondern nur ein zweckmäßig und nicht zweckmäßig im bestimmten Fall.

Aktualisierungsdaten erhalten - Push- oder Pull-Methode?

Für die Art und Weise, wie Observer die benötigten Informationen erhält, gibt es zwei Varianten.

Im Push-Modell übergibt das Subject der update()-Methode detaillierte Informationen über die Änderung als Parameter:

Aktualisierungsmethode nach dem Push-Modell:

public interface Observer1 { 

    public void update(int length, int width, boolean visible, String name); 

} 			

Der Vorteil hierbei ist, dass Observer und Subject noch stärker entkoppelt sind, da der Observer keine Informationen über das Subject benötigt. Auch ist nur ein Methoden-Aufruf zur Übergabe der Informationen nötig. Das Problem bei dieser Vorgehensweise liegt jedoch in den unterschiedlichen Bedürfnissen der Observer begründet: Nicht jeder Observer benötigt zwangsläufig alle Parameter, die ihm übergeben werden. Es kann unnötiger Datentransfer entstehen. Weiterhin werden Erweiterungen erschwert: Was ist, wenn die update()-Schnittstelle um einen weiteren Parameter erweitert werden soll, da ein Observer plötzlich mehr Informationen benötigt? Es müssen alle konkreten Observer angepasst werden. Abhilfe könnte in diesem Fall die Nutzung eines speziellen (Event-)Objektes als Paramter statt der Übergabe der einzelnden Parameter: Dieses Objekt kann alle notwendigen Daten kapseln. Weitere Updateinformationen könnten einfach dem Objekt hinzugefügt werden, ohne dass die Clients brechen. Dieses Vorgehen ist der klassische Ansatz von zahlreichen GUI-Bibliotheken (wie Swing): Eventobjekte kapseln die Änderungsinformation:

Eventobjekt als Parameter der update()-Methode am Beispiel von java.awt.event.ActionListener (weitere Information zu dieser hybriden Lösung unter AWT/Swing Eventhandling):

public interface ActionListener extends EventListener { 

    /** 
     * Invoked when an action occurs. 
     */ 
    public void actionPerformed(ActionEvent e); 

} 			

Beim Pull-Modell erhält der Observer nur eine minimale Benachrichtigung und muss sich die benötigten Informationen selber aus dem Subject holen. Dazu erhält/besitzt es eine Referenz auf das ConcreteSubject (entweder in einer Instanzvariable beim Registrieren gespeichert oder via Argument der update()-Methode).

Aktualisierungsmethode nach dem Pull-Modell:

public interface Observer2 { 

    public void update(ConcreteSubject concreteSubject); 

}  			

Bekommt der Observer nun die Aktualisierungsnachricht, so holt er sich die benötigten Informationen mittels Getter vom konkreten Subject selbst. Damit ist gewährleistet, dass jeder Observer nur die Informationen erhält, die er auch wirklich benötigt. Außerdem werden problematische Situationen entschärft, in denen ein Observer mehrere gleiche Subjects beobachtet. Beim Push-Verfahren wäre unklar, von welchem Subject das Update kommt. Das Pull-Modell ermöglicht es die update()-Schnittstelle stabil zu halten: benötigt ein Observer mehr Informationen, so muss er lediglich ein Getter mehr auf dem ConcreteSubject aufrufen. Der Code der anderen Observer bleibt unangetastet. Allerdings kann das Pull-Modell ineffizient werden, da der Observer ohne Hilfe herausfinden muss, was sich konkret geändert hat.

Merke: Wenn das Subject Aussagen über die Bedürfnisse seiner Observer treffen kann (beispielsweise, wenn nur ein Observer existiert oder einige gleichartige), dann ist das Push-Modell zu bevorzugen. Weiß das Subject aber nichts über seine Observer (beispielweise, wenn es viele verschiedenartige Observer sind), dann sollte das Pull-Modell realisiert werden.

Subject: Abstrakte Superklasse oder Interface?

Die Subjectschnittstelle kann auch ein Interface statt einer abstrakten Klasse sein. Dadurch ist man gezwungen, den oft generischen Administrations- und Aktualisierungscode in jeder Subjectimplementation neu zu schreiben, statt den entsprechenden Code von einer abstrakten Superklasse zu erben. Obwohl dies Kohäsionsverlust bedeutet, kann solch ein Vorgehen in Fällen sinnvoll sein, in den kein allgemeiner Administrations- und Aktualisierungscode von den konkreten Subjects abstrahiert werden kann, da sie zu unterschiedlich sind.

Natürlich kann auch keine Schnittstelle für das Subject definiert werden und Observer können gleich gegen ein konkretes Subject arbeiten. Dies ist zweckmäßig, wenn das Subject nicht ausgetauscht werden muss.

Subject: Ort des Verwaltungscodes

Wohl überlegt sollte auch sein, in welcher Klasse die Verwaltungsmethoden (registerObserver(), unregisterObserver(), notifyOberserver() etc.) für die Observer sein sollen. Drei Möglichkeiten wären denkbar:

Observer Design Pattern: Ort des Verwaltungscodes im Subject

  • Direkt im konkretem Subject.
    • + Einfach und verständlich.
    • + Platz der Superklasse verfügbar (bei Einfachvererbung)
    • - Kohäsionsverlust (Vermischung von Verwaltungscode und Subjectlogik)
  • Abstrakte Subjectsuperklasse.
    • + Schlanker Code der konkreten Subjects
    • + Wiederverwendbarkeit des Verwaltungscodes
    • - Platz der Superklasse vergeben (bei Einfachvererbung)
    • - Probleme beim Pull-Modell. Update()-Methode ist mit der abstrakten Subjectsuperklasse parametrisiert. Observer müssen Fallunterscheidungen und Downcasts durchführen.
  • Extra-Helferklasse, die von dem Subject aggregiert wird. Das Subject delegiert Verwaltungsaufrufe an die Helferklasse, die die gesamte Verwaltungslogik kapselt.
    • + Hohe Kohäsion des Subjects, dank Delegation.
    • + Wiederverwendbarkeit des Verwaltungscodes.
    • + In Programmiersprachen ohne Mehrfachvererbung vergiebt man sich damit nicht den Platz der Superklasse.

Anstoß der Aktualisierung durch Client oder Subject selbst?

Das initiale Auslösen der Observerbenachrichtigung (notifyObservers()) kann sowohl vom Subject selber, als auch vom Client durchgeführt werden.

Ruft das Subject nach jeder Zustandsveränderung (beispielsweise in einem Setter) selbstständig notifyObservers() auf, so kann es nie passieren, dass es vergessen wird, wenn der Client (oder sonst wer) den Subjectzustand ändert. Allerdings kann dieses Vorgehen zu unnötigen Aktualisierungen führen, wenn mehrere Subjectzustände nacheinander geändert werden und jedes Mal alle Observer benachrichtigt werden. Soll der Client selber notifyObservers() aufrufen, so kann dies nicht passieren, da er erst nachdem er alle Subjectzustände modifiziert hat, die Methode aufrufen kann. Die Gefahr ist allerdings groß, dass dies vergessen wird.

Im Sinne einer guten Kapselung sollte die notifyObservers()-Methode entsprechend ihrem Aufrufer entweder public (Client) oder gar protected (ConcreteSubject) deklariert werden.

Anwendungsfälle

  • In Fällen in denen die Veränderung eines Objekts die Modifikation eines oder mehrerer Objekte nötig macht, wobei keine Aussage über die Anzahl der zu ändernden Objekte gemacht werden kann.
    • GUIs (User verändert Daten, neue Daten müssen in allen GUI-Komponenten aktualisiert werden).
    • Ein Datensatz (Key-Value-Paare) mit mannigfaltigen Visualisierungen (Tabelle, Balkendiagramm, Tortendiagramm etc.) ([GoF], Seite 288).
    • Im MVC-Pattern (Model-View-Controller) bei der View-Model-Kommunikation.
    • Bei jedem Sekundentakt eines Zeitgeber muss sowohl die Digitaluhr als auch die Analoguhr aktualisiert werden ([GoF], Seite 297ff.).
  • Szenarien, in denen Objekte andere Objekte benachrichtigen sollen, ohne dabei näheres über das zu benachrichtigende Objekt zu wissen. Das Observer Pattern ermöglicht diese gewünschte lose Kopplung der Objekte.
  • Wenn eine Abstraktion Aspekte umfasst, die von einander abhängig sind, lassen sich diese Aspekte in Subject und Observer kapseln und können somit frei variiert und wiederverwendet werden.
    • Trennung der GUI-Komponenten von der aufzuführenden Aktion nach einem Event (beispielsweise Knopfdruck) auf dieser Komponente.

Vorteile

  • Zustandskonsistenz. Die Daten im Gesamtsystem bleiben konsistent, da die Observer ihren Zustand automatisch bei Änderung des Subjects anpassen. Weiterhin bleibt der Informationsaustausch bzw. die Kopplung zwischen den Objekten auf die Zeit zwischen An- und Abmelden des Observers am Subject begrenzt.
  • Flexibilität und Modularität. Das System erlaubt es, dass mehrere verschiedene Observer ein einziges Subject beobachten, aber auch dass ein Observer mehrere Subjects beobachtet. Weiterhin können Klassen sowohl Observer als auch Subject in einem seien.
    Davon abgesehen muss nicht im Voraus bekannt sein, wie viele und welche Observer sich zur Laufzeit registrieren, da das Subject nur das Interface der Observer kennt und beliebig Observer aufnehmen kann. Das minimiert den Änderungsaufwand, wenn neue Observer hinzugefügt werden - sowohl im Entwicklungsprozess als auch in der späteren Wartungs- und Erweiterungsphase.
  • Wiederverwendbarkeit. Durch das Observer Pattern lassen sich Subject und Observer unabhängig voneinander variieren. Somit kann das Subject wiederverwendet werden, ohne seine abhängigen Observer verwenden zu müssen und umgekehrt. Ebenso sind bestehende Subjects wiederverwendbar, wenn neue Observer hinzugefügt werden.
  • Kompatibilität zum Schichtenmodell. Subject und Observer sind lose und abstrakt gekoppelt, da das Subject keine konkreten Observer kennt, sondern nur die Observerschnittstelle. Somit können sie in verschiedenen Abstraktionsschichten eines Systems liegen (beispielsweise Subject in unterer Schicht und Observer in oberer Schicht): Das Schichtenmodell bleibt konsistent.

Nachteile

  • Aktualisierungskaskaden und -zyklen. Bei umfangreichen Systemen mit vielen Subjects und Observer kann es schnell zu ganzen Aktualisierungskaskaden kommen, da die Observer nichts von einander wissen und nicht abschätzen können, welche Folgen eine einzige Modifikation an einem Subject hat.

    Gefahr von Kaskaden und Zyklen beim Observer Pattern

    Frei nach [Scherer], Folie 12

    Eine Änderung kann so eine ganze Änderungskette nach sich ziehen (Kaskade) oder im schlimmsten Fall zu sich rekursiv wiederholenden Aufrufen führen (Zyklen).
    Davon abgesehen steigt bei komplexen Subject-Observer-Interaktionen die Gefahr von unnötigen Aktualisierungen. Daraus resultierende Fehler sind nur schwer ausfindig zu machen.
  • Abmeldung von Observer. Schnell vergisst man ein Observer beim Subject abzumelden, wenn man es nicht mehr braucht. Dies kann in Fällen von Mehrfachanmeldung (Mehrfachbenachrichtigung) merkwürdige Effekte zur Folge haben und verhindert die automatische Speicherfreisetzung (Garbage Collection in Java und C#), da das Subject immer noch eine Referenz auf ein nicht mehr gebrauchtes Objekt hält.

Anwendung in der Java Standardbibliothek

In der Java API wird das Observer Design Pattern an zahlreichen Stellen angewandt.

AWT/Swing Eventhandling

Das klassische Anwendungsbeispiel in der Java-API ist das Eventhandling von AWT/Swing. Die GUI-Komponenten sind dabei die Subjects, bei den sich Listener, die Observer, registrieren können. Findet eine Userinteraktion auf der Komponente statt, so werden alle registrierten Listener benachrichtigt.

Der Aufbau wird im folgendem am Beispiel von Buttons und dem dazugehörigen Action- und ChangeListenern dargestellt.

AWT/Swing Eventhandling nutzt das Observer Design Pattern

Jeder Swing-Button erbt von AbstractButton Methoden zum An- und Abmelden von Listenern, sowie eine Methode fireActionPerformed() (bzw. fireStateChanged()) zur Benachrichtigung der entsprechenden Listener. Diese müssen die Aktualisierungsmethode actionPerformed(ActionEvent) bzw. stateChanged(ChangeEvent)) implementieren.

Die Methode fireActionPerformed(ActionEvent) aus AbstractButton:

public abstract class AbstractButton extends JComponent implements ItemSelectable,SwingConstants { 
    // viel Code... 
    /** 
     * Notifies all listeners that have registered interest for 
     * notification on this event type.  The event instance 
     * is lazily created using the event 
     * parameter. 
     * 
     * @param event  the ActionEvent object 
     * @see EventListenerList 
     */ 
    protected void fireActionPerformed(ActionEvent event) { 
        // Guaranteed to return a non-null array 
        Object[] listeners = listenerList.getListenerList(); 
        ActionEvent e = null; 
        // Process the listeners last to first, notifying 
        // those that are interested in this event 
        for (int i = listeners.length - 2; i >= 0; i -= 2) { 
            if (listeners[i] == ActionListener.class) { 
                // Lazily create the event: 
                if (e == null) { 
                    String actionCommand = event.getActionCommand(); 
                    if (actionCommand == null) { 
                        actionCommand = getActionCommand(); 
                    } 
                    e = new ActionEvent(AbstractButton.this, ActionEvent.ACTION_PERFORMED, 
                                    actionCommand, event.getWhen(), event.getModifiers()); 
                } 
                ((ActionListener)listeners[i + 1]).actionPerformed(e); 
            } 
        } 
    } 
    // viel Code... 
} 			

Interessant ist der Einsatz von EventObjects als Parameter für die Updatemethoden. So kreiert fireStateChanged() ein ChangeEvent-Objekt mit Angaben zum Event und übergibt es beim Aufruf von stateChanged(ChangeEvent) auf jeden Listener. Solche EventObjects kombinieren die Push- mit der Pullmethode: EventObjects enthalten zum einen Informationen über das Event (getActionCommand(), getWhen() und getModifiers() beim ActionEvent), zum anderen erlangt man über getSource() immer ein Handle auf die auslösende GUI-Komponente (konkretes Subject) und kann aus diesem die benötigten Informationen herausziehen oder die Komponente modifizieren (beispielsweise mit getText() und setText()).

Swing: JList und JTable

Bei Listen in Swing dient das ListModel als Subject und JList als Observer (genauer gesagt hat JList eine innere Klasse AccessibleJList, die als Observer fungiert).

Swing JList und JTable realisiert das Observer Design Pattern

Wird ein Element in einem Listenmodell hinzugefügt oder entfernt, so wird eine Benachrichtigung (intervalAdded(), intervalRemoved() oder contentsChanged()) an die JList gesendet und diese stellt das neue Element da. Beim DefaultListModel (erweitert AbstractListModel und implementiert damit ListModel) sieht man dieses Verhalten sehr gut an der addElement(Object)-Methode, mit dem ein Element in die Liste hinzugefügt werden kann:

Die Methode addElement() von DefaultListModel löst die Benachrichtigung der Listener aus:

public class DefaultListModel extends AbstractListModel { 

    private Vector delegate = new Vector(); 

    //viel Code... 
    /** 
     * Adds the specified component to the end of this list. 
     * 
     * @param   obj   the component to be added 
     * @see Vector#addElement(Object) 
     */ 
    public void addElement(Object obj) { 
        int index = delegate.size(); 
        delegate.addElement(obj); 
        fireIntervalAdded(this, index, index); 
    } 
    //viel Code... 

}  			

Daraufhin, wird die Methode fireIntervalAdded() der abstrakten Superklasse AbstractListModel aufgerufen und der ListDataListener in JList benachrichtigt.

Die Methode fireIntervalAdded() von AbstractListModel benachrichtigt alle Listener:

public abstract class AbstractListModel implements ListModel, Serializable { 

    protected EventListenerList listenerList = new EventListenerList(); 

    //viel Code... 
    /** 
     * AbstractListModel subclasses must call this method 
     * after
     * one or more elements are added to the model.  The new elements 
     * are specified by a closed interval index0, index1 -- the enpoints 
     * are included.  Note that 
     * index0 need not be less than or equal to index1. 
     * 
     * @param source the ListModel that changed, typically "this" 
     * @param index0 one end of the new interval 
     * @param index1 the other end of the new interval 
     * @see EventListenerList 
     * @see DefaultListModel 
     */ 
    protected void fireIntervalAdded(Object source, int index0, int index1) { 
        Object[] listeners = listenerList.getListenerList(); 
        ListDataEvent e = null; 

        for (int i = listeners.length - 2; i >= 0; i -= 2) { 
            if (listeners[i] == ListDataListener.class) { 
                if (e == null) { 
                    e = new ListDataEvent(source, ListDataEvent.INTERVAL_ADDED, index0, index1); 
                } 
                ((ListDataListener)listeners[i + 1]).intervalAdded(e); 
            } 
        } 
    } 

    //viel Code... 
} 			

Kommentare

Bitte auswählen:*
tttt 2016-07-11 17:56:12
Danke für die Hilfe
Steffen 2016-05-02 10:54:32
Schön aufgearbeitet!

Kleiner Hinweis: Ein Laufzeitfehler (ConcurrentModificationExcepti on) wird immer dann auftreten, wenn sich der konkrete Observer innerhalb der Methode update() durch den Aufruf von unregister(...) vom beobachteten Subject abmeldet. Das wäre immer dann der Fall, wenn der Beobachter nur einmalig über eine Zustandsänderung informiert werden möchte.
Ray 2016-01-07 10:53:18
Zuerst mal DANKE für deine Erklärungen, verstehe die Pattern nun richtig gut :) Würde mich freuen, wenn du deine Design Pattern Liste erweiterst :)
Frage zu der Extra-Helferklasse:
Was wäre deiner Meinung ein guter Ansatz für die Helferklasse?

Ich denke da eher an:
static ObserverAdministration{

// Einfach damit die Administration weiß, welches SubjectArt er bekommt
static register(IObserver, ObserverList){}
static notify(ObserverList,pState){}
}

Oder siehst du an diesem Ansatz eventuelle Probleme?
botanical slimming 2014-10-04 08:30:09
I usually turn out experiencing Grooveshark though.
Christoph Tornau 2014-09-19 15:04:36
Herzlichen Dank für diese anschauliche und ausführliche Erklärung. Von mir stammte ein anderer Artikel über das Observer Pattern (Beobachter Muster). Eventuell wäre es möglich, einen Link zu weiterführenden Erklärungen hier einzufügen. Meine URL lautet: www.tornau.name/2014/02/das-ob server-pattern-beobachter-must er/
Steve 2014-01-06 11:51:14
Wirkliche gelungene Erklärung. Wie sieht es mit Threadsicherheit aus?
Also ein Thread removed einen Observer während das Subject über die
Oberserver mittels for each iteriert?
SL 2013-08-10 19:46:50
Super Erklärung. Für einen älteren Herren wie mich genau das richtige für die Wieder-Einarbeitung ins Observer-Pattern nach einigen Jahren "Software-Archäologie".
Beobachter^^ 2013-07-07 13:14:15
Danke! Finde ihre Arbeit beeindruckend und sehr hilfreich :)!
Sebi 2012-11-15 11:40:36
Super! Genau das habe ich gesucht. Sehr schön und übersichtlich erklärt.

Gruß
Stefan 2012-09-10 15:26:03
Schönen Dank für die verständlichen Beispiele. Hab einen kleinen Tippfehler gefunden.
Im Oberserver Pattern:
Beim Push-Verfahren wäre unkar,

soll wohl \"unklar\" sein
B2 2012-05-08 10:53:17
Danke, das Beispiel war super verständlich und hat eine Menge Fragen geklärt!
Philipp 2012-02-13 09:19:43
Hallo r,

ich bin immer wieder überrascht, welche Ausmaße Betriebsblindheit annehmen kann. Kurzum: Natürlich heißt es Pull (von \"ziehen\", die Parameter werden ja aus dem Subjekt rausgezogen... mit einer Abstimmung hat das nichts zu tun). Dankeschön für deinen Hinweis. :-)
r 2012-02-13 00:48:04
Vielen Dank für die echt sehr verständlichen Erklärungen!
Eine kleine Anmerkung: sollte es nicht statt \"poll\"-Modell \"pull\"-Modell heißen?
Heißt zumindest bei uns in der Vorlesung so...
lg
Stefan 2012-02-12 23:18:33
Super Philipp, die beste Observer Erklärung die ich bis jetzt gelesen habe. Danke Dir, Stefan
Philipp 2012-02-04 22:47:00
Hallo Carsten,

vielen Dank für deinen Hinweis. Du beschreibst den klassischen Ansatz, den auch in zahlreichen GUI-Bibliotheken mit den Eventobjekten gefahren wird: Das Eventobjekt kapselt alle Informationen zum Update und macht damit die Clients stabil gegen Änderungen an den übergebenen Daten. Warum ich bis jetzt an dieser Stelle noch nicht darauf eingegangen bin, ist mir schleierhaft. Jetzt ist es jedenfalls drin und ich danke dir für deinen Hinweis.

Philipp

Seite: 1 - nächste Seite