Idee des Patterns
Das Observer-Pattern oder zu Deutsch Beobachter-Muster ist eines der am meisten genutzten und bekanntesten Patterns. In diesem Muster gibt es zwei Akteure: Ein Subjekt, welches beobachtet wird und ein oder mehrere Beobachter, die über Änderungen des Subjektes informiert werden wollen.
Würde man ohne das Pattern eine solche Beobachtung implementieren, so müssten die beobachtenden Objekte in regelmäßigen Abständen das beobachtete Subjekt anfragen, ob sich sein Zustand geändert hat. Durch dieses Vorgehen wird unnötig Rechenzeit verschwendet.
Das Objektdiagramm sähe wie folgt aus, wenn das Observer-Pattern noch nicht angewendet worden ist:
Die Idee des Observer-Patterns ist es nun, dem zu beobachtenden Subjekt die Aufgabe aufzutragen, die Beobachter bei einer Änderung über die Änderung zu informieren. Die Beobachter müssen nicht mehr in regelmäßigen Abständen beim Subjekt anfragen, sondern können sich darauf verlassen, dass sie eine Nachricht über eine Änderung erhalten.
Nun sieht das Objekt-Diagramm nach der Anwendung des Observer-Patterns wie folgt aus:
Registrierung, Benachrichtigung
Beobachter müssen sich, bevor Sie von einem Subjekt benachrichtigt werden, bei diesem Subjekt registrieren. Jedes Subjekt verwaltet intern eine Liste von Beobachtern, die es bei einer Änderung seiner selbst nacheinander benachrichtigt. Neben der Methode zum Registrieren wird standardmäßig auch eine Methode zum Deregistrieren angeboten. Über diese Methode können sich Beobachter wieder abmelden, so dass sie aus der internen Liste entfernt werden und nicht mehr benachrichtigt werden.
Den Ablauf des Registrierens und der Benachrichtigung verdeutlicht das folgende Sequenzdiagramm (Es sei hier darauf hingewiesen, dass die Rücknachrichten in diesem Diagramm ausgelassen worden sind. Sie müssten zur Vollständigkeit ergänzt werden.):
Push und Pull
Wenn sich der Zustand des Subjektes ändert, ist für die meisten Beobachter der neue Zustand des Subjektes interessant. Hier lassen sich nun zwei Strategien umsetzen: Das Subjekt kann entweder den geänderten Zustand schon bei der Benachrichtigung des Beobachters mitsenden (Push-Methode). Oder aber der Beobachter kann, sobald er eine Nachricht erhält, dass sich der Zustand des Subjektes geändert hat, selbst aktiv werden und das Subjekt nach seinem neuen Zustand über einen Methodenaufruf befragen (Pull-Methode). Die Push-Methode hat zum Nachteil, dass es eventuell sein kann, dass das Subjekt Informationen sendet, die der Beobachter nicht verwerten kann oder will. Dies ist vor allen Dingen für sehr große Subjekte, die viele Zustände beinhalten der Fall. Auch muss für diesen Fall ein geeignetes Austauschformat, meist eine eigene Klasse, in deren Objekte der Zustand verpackt wird, definiert werden. Die Methode, dass die Beobachter das Subjekt befragen, hat zum Nachteil, dass entsprechende Methoden im Subjekt definiert werden müssen.
Abstrakte Darstellung als UML2-Klassendiagramm
Quellcode in Java
Ohne die Java-API zu nutzen, kann das Observer-Pattern (Beobachter-Muster) wie folgt in Java ausimplementiert werden:
1 2 3 4 5 6 7 8 |
/** * Subjekt.java */ public interface Subjekt { public abstract void addBeobachter(Beobachter beobachter); public abstract void removeBeobachter(Beobachter beobachter); public abstract void notifyAlleBeobachter(); } |
1 2 3 4 5 6 |
/** * Beobachter.java */ public interface Beobachter { public abstract void update(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/** * KonkretesSubjekt.java */ import java.util.ArrayList; import java.util.List; public class KonkretesSubjekt implements Subjekt { List beobachterList = new ArrayList(); int state = 0; @Override public void addBeobachter(Beobachter beobachter) { this.beobachterList.add(beobachter); } @Override public void removeBeobachter(Beobachter beobachter) { this.beobachterList.remove(beobachter); } @Override public void notifyAlleBeobachter() { for (Beobachter beobachter : beobachterList) { beobachter.update(); } } public int getState() { return state; } public void setState(int state) { this.state = state; this.notifyAlleBeobachter(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * KonkreterBeobachter.java */ public class KonkreterBeobachter implements Beobachter { private KonkretesSubjekt konkretesSubjekt; public KonkreterBeobachter(KonkretesSubjekt konkretesSubjekt) { this.konkretesSubjekt = konkretesSubjekt; // Durchführung der Registrierung beim übergebenen Subjekt this.konkretesSubjekt.addBeobachter(this); } @Override public void update() { int newState = konkretesSubjekt.getState(); // ...auf neuen Status reagieren } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * Client.java */ public class Client { public static void main(String[] args) { // Erzeugung des Subjektes KonkretesSubjekt konkretesSubjekt = new KonkretesSubjekt(); // Erzeugung des Beobachters. Dabei wird // das Subjekt übergeben und registriert. KonkreterBeobachter konkreterBeobachter = new KonkreterBeobachter(konkretesSubjekt); // Zustandsänderung, Subjekt benachrichtigt // daraufhin die Beobachter konkretesSubjekt.setState(1); } } |
Zum Nachvollziehen ist der Source-Code auf GitHub unter ObserverPattern verfügbar.
Interfaces vs. Abstrakte Klassen
Die Java-API verfügt über ein Interface Observer und eine abstrakte Klasse Observable, die als Basis für das Observer-Pattern genutzt werden können. Durch diese Umsetzung des Observer-Patterns wird klar, dass das oben genannte Interface Subjekt auch durch eine abstrakte Klasse ersetzt werden kann. Dies abstrakte Klasse Observable innerhalb der Java-API beinhaltet schon Methoden, um Beobachter zu benachrichtigen, so dass Code gespart werden kann. Insgesamt ändert sich das Klassendiagramm wie folgt:
Der Quellcode der die Java-API nutzt, sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * KonkretesSubjekt.java */ import java.util.Observable; public class KonkretesSubjekt extends Observable { int state = 0; public int getState() { return state; } public void setState(int state) { this.state = state; this.setChanged(); this.notifyObservers(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * KonkreterBeobachter.java */ import java.util.Observable; import java.util.Observer; public class KonkreterBeobachter implements Observer { private KonkretesSubjekt konkretesSubjekt; public KonkreterBeobachter(KonkretesSubjekt konkretesSubjekt) { this.konkretesSubjekt = konkretesSubjekt; // Durchführung der Registrierung beim übergebenen Subjekt this.konkretesSubjekt.addObserver(this); } @Override public void update(Observable o, Object arg) { int newState = konkretesSubjekt.getState(); // ...auf neuen Status reagieren } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * Client.java */ public class Client { public static void main(String[] args) { // Erzeugung des Subjektes KonkretesSubjekt konkretesSubjekt = new KonkretesSubjekt(); // Erzeugung des Beobachters. Dabei wird // das Subjekt übergeben und registriert. KonkreterBeobachter konkreterBeobachter = new KonkreterBeobachter(konkretesSubjekt); // Zustandsänderung, Subjekt benachrichtigt // daraufhin die Beobachter konkretesSubjekt.setState(1); } } |
Wichtig ist, dass die setChanged()-Methode aufgerufen wird, bevor die Observer benachrichtigt werden. Wird dies nicht getan, so reagiert die abstrakte Oberklasse Observable nicht auf die Änderung, weil sie denkt, dass sich das Subjekt nicht geändert hat.
Eine negative Seitenerscheinung dieser Implementierung ist es, dass das konkrete Subjekt von keiner weiteren Klasse mehr erben kann als der Klasse Observable. Dadurch muss auf eine Delegation ausweichen, wenn es schon vor der Einführung des Observer-Patterns hier eine Vererbung gegeben hat.
Zum Nachvollziehen ist der Source-Code auf GitHub unter ObserverPatternJDK verfügbar.
Das Beobachtermuster im Video erklärt
Java 8 Sprachfeatures
In Java 8 kommen zum Sprachstandard von Java sogenannte Default-Methoden hinzu, die in den Interfaces implementiert werden. Diese Default-Methoden eignen sich jedoch nicht für die Implementierung des Observer-Patterns, da innerhalb der Interfaces keine Klassen-Attribute deklariert werden können.
Wohl aber lassen sich Lambdas verwenden, um Beobachter kompakt darzustellen. Ein Lambda-Ausdruck ist eine weitere verkürzte Schreibweise einer anonymen Klasse. Hier definieren wir einen Beobachter als anonyme Klasse. Dies ist auch schon mit Java 7 möglich:
1 2 3 4 5 6 |
konkretesSubjekt.addBeobachter(new Beobachter() { @Override public void update() { System.out.println ("State changed"); } }); |
Dies kann in Java 8 mit einem Lambda-Ausdruck weiter verkürzt werden:
1 |
konkretesSubjekt.addBeobachter(() -> System.out.println ("State changed")); |
Die beiden runden Klammern () dienen zur Parameterübergabe. Hier können Parameter, die bei dem Aufruf des Beobachters übergeben werden, angegeben werden. Dazu erweitern wir zunächst das Interface Beobachter
1 2 3 4 5 6 |
/** * Beobachter.java */ public interface Beobachter { public abstract void update(int pushvalue); } |
Nun können wir einen Lambda-Ausdruck wie folgt formulieren:
1 2 |
konkretesSubjekt.addBeobachter((pushvalue) -> System.out.println ("State changed to " + pushvalue)); |
Sowohl beim Nutzen einer anonymen Klasse, wie auch bei der Nutzung eines Lambda-Ausdruckes entsteht das Problem, dass wir keine Variable mehr erhalten, in welcher das Objekt des Beobachters vorhanden ist. Wir können den Beobachter nicht mehr vom Subjekt abmelden, da wir keine Referenz haben. Dieses Problem lässt sich jedoch umgehen, indem wir bei der Anmeldung des Beobachters den angemeldeten Beobachter selbst als Rückgabewert zurückgeben. Dafür erweitern wir zunächst das Interface für das Subjekt, so dass addBeobachter ein Objekt mit dem Typ Beobachter zurück gibt:
1 2 3 4 5 6 7 |
/** * Subjekt.java */ public interface Subjekt { public abstract Beobachter addBeobachter(Beobachter beobachter); /* ... weitere Methodenköpfe ... */ } |
Dann erhalten wir den Beobachter sowohl bei der Registrierung durch eine anyonyme Klasse, wie auch durch einen Lambda-Ausdruck:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Beobachter anonymeKlasseBeobachter = konkretesSubjekt.addBeobachter(new Beobachter() { @Override public void update() { System.out.println ("State changed"); } }); Beobachter lambdaBeobachter = konkretesSubjekt.addBeobachter(() -> System.out.println ("State changed")); // Diese Beobachter können wir nun wieder deregistrieren, // da wir eine Objektreferenz halten konkretesSubjekt.removeBeobachter(anonymeKlasseBeobachter); konkretesSubjekt.removeBeobachter(lambdaBeobachter); |
Zum Nachvollziehen ist der Source-Code auf GitHub unter ObserverPatternJDK8 verfügbar.
Hooks als eine Vereinfachung des Patterns für nur einen Observer
Ein Hook ist eine Methode, welche in der Oberklasse einer Klasse mit einem leeren Methodenrumpf implementiert ist. Damit ist die Methode nicht abstrakt, sondern hat nur keinen Code, der bei ihrer Ausführung durchlaufen wird. Eine Unterklasse kann, muss jedoch nicht, diese Methode überschreiben.
Mit einem Hook kann sich eine Unterklasse eines Subjektes wie ein Beobachter verhalten, muss es aber nicht.
In JavaScript wird das Hook-Konzept massiv für Beobachter genutzt, indem dynamisch Methoden von DOM-Objekten überschrieben werden. So kann genau eine Funktion beispielsweise bei einem Klick auf ein DOM-Element aufgerufen werden.
Probleme des Observer-Patterns und deren Lösung durch AOP
Eines der Probleme des Observer-Patterns ist es, dass eventuell bei einer Zustandsänderung einer Modelklasse sehr viele Observer benachrichtigt werden. Die Beobachter brauchen jeder für sich Rechenzeit, um beispielsweise ihre Anzeige aktualisieren zu können, was das Programm verlangsamt. Wenn nun dieser neue angezeigte Zustand nur ein Zwischenzustand ist, wie dies beispielsweise bei einem Hinzufügen von einem Element nach dem anderen in eine Liste geschieht, wäre die Aktualisierung der Anzeige überhaupt nicht nötig gewesen. Dies kann sich negativ auf die Performance des Gesamtprogrammes auswirken, bis sogar dahin, dass das Programm unbenutzbar wird und nur noch „flackert“. Hier gibt AOP Abhilfe. AOP kann den dynamischen Kontrollfluss des Programmes mit Hilfe eines Aspektes überwachen und feststellen, dass ein Kontrollflussblock verlassen worden ist und dass eine Serie von Änderungen abgeschlossen ist. Änderungsmitteilungen können zurückgehalten werden, bis dieser Moment erreicht ist.
Auch das Problem, dass bei einer allgemeinen Implementierung, wie in der Java API vorhanden, eine Vererbung einer abstrakten Klasse geschehen muss, lässt sich mit Hilfe der aspektorientierten Programmierung lösen. So lässt sich die abstrakte Implementierung des Observer-Patters komplett von seiner konkreten Implementierung trennen, ohne Nachteile in Kauf nehmen zu müssen. Dabei wird ein Aspekt erstellt, der sämtliche Observable-Methoden des Observer-Patterns in die entsprechende konkrete Klasse des Observable injiziert.
Vielen Dank für die super Erklärung!
Wirklich prima gemacht! Weiter so!!!
gute erklärung!
Das Klassendiagram zu letzt sollte, wenn es schon beschrieben ist, dass observable abstract ist dann sollte man dies auch im Klassendiagram durch entsprechenden Stereotyp kenntlich machen, es führt sonst ein wenig zu verwirung.
sonst ganz gut.
Herzlichen Dank für das Lob. Ja, Observable im letzten Diagramm ist abstrakt. Innerhalb des Klassendiagrammes ist dies jedoch auch schon markiert. Es ist kursiv geschrieben, was bedeutet, dass die Klasse abstrakt ist. Nur bei einer handschriftlichen Notation kann man – da hier kursiv nicht unbedingt von normaler Schrift zu unterscheiden ist – noch {abstract} in die oberste Box der Klasse einfügen.
Hervorragend! So klar und deutlich kann ein eigentlich so einfaches Thema erläutert werden, wenn man will. Großes Lob!
Danke für Ihre ausführliche Erklärung!
Grosses Lob! Weiter so!!!!!!!!!!!!!!!!
Hallo Christoph,
klasse für die tolle Erklärung. Bei der Methode „public void notifyAlleBeobachter()“ hänge ich aber.
Wie implementiert man genau den Aufruf, wenn man zum Beispiel 3 Beobachter hat.
Danke und Grüsse
Michael
Hallo Michael,
der Aufruf ist sehr einfach zu implementieren. Einfach die Methode
notifyAlleBeobachter()
aus dem übrigen Code aufrufen. 😉 Innerhalb der MethodenotifyAlleBeobachter()
findet dann das Iterieren über die Beobachter statt. Damit braucht sich der aufrufende Code um die Anzahl der Beobachter nicht mehr zu kümmern. Er verliert die Abhängigkeit dazu (Inversion of Control).Gruß
Christoph