(Credits: Blog von Vishal Chovatiya)
„Das Observer Pattern ermöglicht es einem Objekt, mehrere interessierte Objekte automatisch über Änderungen seines Zustands zu informieren, ohne deren konkrete Implementierungen kennen zu müssen.”
Das Observer Pattern gehört zur Kategorie der Verhaltensmuster (Behavioral Patterns) und löst das Problem, wie mehrere Objekte (Observer) über Zustandsänderungen eines anderen Objekts informiert werden können, ohne dass eine enge Kopplung zwischen ihnen entsteht.
Im Zentrum steht ein sogenanntes Subject (auch Publisher genannt), das eine Liste von Observern (auch Subscriber) verwaltet und diese bei relevanten Änderungen benachrichtigt. Observer können sich zur Laufzeit dynamisch an- und abmelden, wodurch das System sehr flexibel bleibt.
Das Subject kennt dabei nur die abstrakte Schnittstelle der Observer, nicht deren konkrete Implementierung – dies entspricht dem Prinzip der losen Kopplung (Loose Coupling). Dadurch lassen sich neue Observer-Typen hinzufügen, ohne den Code des Subjects verändern zu müssen, was dem Open-Closed-Prinzip entspricht.
Typische Anwendungsfälle sind grafische Benutzeroberflächen (z. B. Event-Handling), Modell-View-Architekturen (MVC) oder Publish-Subscribe-Systeme.
In C++ wird das Pattern häufig über eine abstrakte Basisklasse mit einer virtuellen update()-Methode für die Observer
sowie eine Verwaltungsstruktur (z. B. std::vector<std::weak_ptr<IObserver>>) im Subject umgesetzt.
Abbildung 1: Schematische Darstellung des Observer Pattern Musters: An- und Abmelden.
Abbildung 2: Schematische Darstellung des Observer Pattern Musters: Benachrichtigungen.
Abbildung 3: Schematische Darstellung des Observer Pattern Musters: Abstraktion des Observers mittels Schnittstelle IObserver.
Das Conceptual Example liegt in vier Varianten vor:
- Variante 1: Mit
std::shared_ptr- undstd::weak_ptr-Objekten. - Variante 2: Wie Variante 1, aber mit
std::enable_shared_from_this<>Mechanismus. - Variante 3: Mit
std::function<>.
Quellcode 1
Quellcode 2
Quellcode 3
Im „Subject” ist – wie beschrieben – eine Liste mit
den Beobachtern zu verwalten. Hier sollte man beachten,
dass eine Liste mit std::weak_ptr-Zeigern die bessere Wahl
im Vergleich zu std::shared_ptr-Objekten ist.
Anmerkungen zum Design:
ISubjectenthält jetzt nur nochdetach().attach()ist bewusst nicht Teil des Interfaces, weil sein Rückgabetyp (Subject::Connection) an die konkreteSubject-Implementierung gebunden ist — ein generischesISubject::attach()müsste sonst entweder auf eine abstrakteConnection-Basisklasse (Interface-Overhead) oder aufstd::any/Type-Erasure ausweichen. Für dein Lehrbeispiel würde ich es so lassen.- Der Rückverweis
Subject* m_subjectinConnectionist ein roher, nicht-owning Pointer. Das ist hier okay, weilSubjectnon-copyable/non-movable ist und der Client dafür verantwortlich ist, dass alle Connections vor dem Subject zerstört werden (typisches RAII-Lifetime-Constraint, vergleichbar mit Iteratoren vs. Container). Einstd::weak_ptr<Subject>wäre robuster, würde aber voraussetzen, dassSubjectselbst perstd::shared_ptrverwaltet wird — eine größere Design-Entscheidung, die über dieses Beispiel hinausgeht. [[nodiscard]]aufattach()sorgt dafür, dass ein Compiler-Warning auftaucht, falls jemand den Rückgabewert versehentlich verwirft — sonst würde dieConnectionsofort wieder zerstört und derObserverwäre direkt nachattach()schon wieder abgemeldet, ohne dass das offensichtlich wäre.
Verwaltet man std::weak_ptr-Objekte in einem std::list-Container,
kann es zu extrem unverständlichen Übersetzungsfehlern kommen,
zum Beispiel, wenn man ein std::weak_ptr-Objekt aus der Liste entfernen möchte
(Aufruf von remove):
binary '==': 'const _Ty' does not define this operator or a conversion to a type acceptable to the predefined operator
Die Ursache des Fehlers liegt darin begründet,
dass der ==-Operator für std::weak_ptr-Objekte nicht vorhanden ist.
Eine Lösung des Problems besteht darin, von der remove- zur remove_if-Methode
zu wechseln und eine geeignete Lambda-Funktion zu realisieren,
zum Beispiel so:
void detach(std::weak_ptr<IObserver> observer) override {
m_list_observers.remove_if([&](std::weak_ptr<IObserver> wp) {
return !observer.owner_before(wp) && !wp.owner_before(observer);
}
);
}Die Klasse std::weak_ptr besitzt eine owner_before Methode,
die eine Eigentümer-basierte Reihenfolge von std::weak_ptr-Zeigern implementiert.
Der Vergleich funktioniert so:
return !observer.owner_before(wp) && !wp.owner_before(observer);Dies entspricht der Frage: „Zeigen die beiden Zeiger nicht voreinander?”, was bedeutet: „Zeigen sie auf dasselbe Objekt?”
Aufgeschlüsselt:
| Ausdruck | Bedeutung |
|---|---|
observer.owner_before(wp) |
Zeigt observer auf ein Objekt, das vor dem Objekt von wp liegt? |
observer.owner_before(wp) |
Liegt observer nicht vor wp? |
observer.owner_before(wp) |
Liegt wp nicht vor observer? |
Both true |
Es muss sich um dasselbe Objekt handeln. |
Eine detailliertere Beschreibung kann man unter „Removing item from list of weak_ptrs ” nachlesen.
Das Pattern kann alternativ auf der Basis von Schnittstellen auch mit dem Modern C++ Sprachelement std::function realisiert werden.
Auf diese Weise umgeht man die Anforderung, dass sich alle Observer-Klassen von einer gemeinsamen Schnittstelle ableiten,
die die update-Methode realisiert.
Siehe hierzu die Datei ConceptualExample04.cpp.
Die Anregung zum konzeptionellen Beispiel finden Sie unter
https://refactoring.guru/design-patterns
vor.
Hinweis zu shared_from_this:
In Variante 2 wird prinzipiell ohne „raw”-Zeiger gearbeitet, also so,
wie man es der „reinen Lehre” nach machen sollte. Dabei stellt sich aber eine Frage:
Wie erhalte ich von einem Objekt einen std::shared_ptr, desses Objekterzeugung
außerhalb meines Wirkungskreises liegt. Die Frage lautet also gewissermaßen:
std::shared_ptr<IObserver> me = this;Das geht so nicht, wie zu erwarten war. Es gibt aber den folgenden Weg:
class Observer : public IObserver, public std::enable_shared_from_this<Observer> {
...
}Die Klasse Observer leitet sich von einer Standard-Klasse std::enable_shared_from_this<> ab,
die genau zu diesem Zweck realisiert wurde.
Damit vererbt sie eine Methode shared_from_this an die Kindklasse.
Auf diese Weise kann man dann beispielsweise eine Methode removeMeFromTheList
wie folgt implementieren:
void removeMeFromTheList() {
std::shared_ptr<Observer> me = shared_from_this();
m_subject->detach(me);
std::cout << "Observer \"" << m_number << "\" removed from the list.";
}Achtung: Der Aufruf shared_from_this ist aber nur dann zulässig,
wenn es von dem Objekt (hier: Klasse Observer) bereits einen Shared Pointer gibt!
Das muss nicht immer der Fall sein!
Deshalb ist in der vorliegenden Fallstudie zumindest eine Methode im Stile von removeMeFromTheList realisierbar,
aber nicht ein entsprechendes Gegenstück zum Anmelden. Weitere Details der Realisierung
entnehmen Sie bitte der dritten Variante des Conceptual Example.
