Patterns in der Softwareentwicklung: Das Beobachtermuster
Das Beobachtermuster definiert 1-zu-n-Abhängigkeiten zwischen Objekten, sodass Änderungen an einem Objekt Benachrichtigungen der abhängigen Objekte anstoßen.
- Rainer Grimm
Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das Beobachtermuster ist ein Verhaltensmuster aus dem Buch "Design Patterns:Elements of Reusable Object-Oriented Software". Es definiert 1-zu-n-Abhängigkeiten zwischen Objekten, sodass Änderungen an einem Objekt dazu führen, dass alle abhängigen Objekte benachrichtigt werden.
Das Beobachtermuster löst ein klassisches Designproblem: Wie kann man sicherstellen, dass alle Interessenten automatisch benachrichtigt werden, wenn ein wichtiges Ereignis stattgefunden hat?
Das Beobachtermuster
Zweck
- Definiert 1-zu-n-Abhängigkeiten zwischen Objekten, sodass Änderungen an einem Objekt dazu führen, dass alle abhängigen Objekte benachrichtigt werden.
Auch bekannt als
- Publisher-Subscriber (kurz Pub/Sub)
Anwendungsfall
- Ein Objekt hängt vom Zustand eines anderen Objekts ab,
- eine Änderung an einem Objekt zieht eine Änderung an einem anderen Objekt nach sich,
- Objekte sollten über Zustandsänderungen eines anderen Objekts benachrichtigt werden, ohne dass eine enge Kopplung besteht.
Struktur
Subject
- Verwaltet seine Kollektion von Beobachtern
- Erlaubt den Beobachtern, sich selbst zu registrieren und abzumelden
Observer
- Definiert eine Schnittstelle zur Benachrichtigung der Beobachter
ConcreteObserver
- Implementiert die Schnittstelle
- Wird vom
Subject
benachrichtigt
Beispiel
Das folgende Programm observer.cpp
implementiert das vorherige Klassendiagramm.
// observer.cpp
#include <iostream>
#include <list>
#include <string>
class Observer {
public:
virtual ~Observer(){};
virtual void notify() const = 0;
};
class Subject {
public:
void registerObserver(Observer* observer) {
observers.push_back(observer);
}
void unregisterObserver(Observer* observer) {
observers.remove(observer);
}
void notifyObservers() const { // (2)
for (auto observer: observers) observer->notify();
}
private:
std::list<Observer *> observers;
};
class ConcreteObserverA : public Observer {
public:
ConcreteObserverA(Subject& subject) : subject_(subject) {
subject_.registerObserver(this);
}
void notify() const override {
std::cout << "ConcreteObserverA::notify\n";
}
private:
Subject& subject_; // (3)
};
class ConcreteObserverB : public Observer {
public:
ConcreteObserverB(Subject& subject) : subject_(subject) {
subject_.registerObserver(this);
}
void notify() const override {
std::cout << "ConcreteObserverB::notify\n";
}
private:
Subject& subject_; // (4)
};
int main() {
std::cout << '\n';
Subject subject;
ConcreteObserverA observerA(subject);
ConcreteObserverB observerB(subject);
subject.notifyObservers();
std::cout << " subject.unregisterObserver(observerA)\n";
subject.unregisterObserver(&observerA); // (1)
subject.notifyObservers();
std::cout << '\n';
}
Der Observer
unterstützt die Mitgliedsfunktion notify
; das Subject unterstützt die Mitgliedsfunktionen registerObserver
, unregisterObserver
und notifyObservers
. Die konkreten Beobachter erhalten das Subjekt in ihrem Konstruktor und benutzen es, um sich für die Benachrichtigung zu registrieren. Sie haben einen Verweis auf das Subject
(3 und 4). observerA
wird in (1) deregistriert. Die Mitgliedsfunktion notifyObservers
geht alle registrierten Beobachter durch und benachrichtigt sie (2).
Der folgende Screenshot zeigt die Ausgabe des Programms:
Im vorherigen Programm observer.cpp
habe ich bewusst keinen Speicher angefordert. So wird Virtualität gerne eingesetzt, wenn man beispielsweise in eingebetteten Systemen keinen dynamischen Speicher (Heap) verwenden darf. Hier ist ist die entsprechende main
-Funktion mit Speicherzuweisung:
int main() {
std::cout << '\n';
Subject* subject = new Subject;
Observer* observerA = new ConcreteObserverA(*subject);
Observer* observerB = new ConcreteObserverB(*subject);
subject->notifyObservers();
std::cout <<
" subject->unregisterObserver(observerA)" << "\n";
subject->unregisterObserver(observerA);
subject->notifyObservers();
delete observerA;
delete observerB;
delete subject;
std::cout << '\n';
}
Bekannte Verwendungen
Das Beobachtermuster wird häufig in Architekturmustern wie Model-View-Controller (MVC) für grafische Benutzeroberflächen oder Reaktor für die Ereignisbehandlung verwendet.
- Model-View-Controller: Das Modell repräsentiert die Daten und ihre Logik. Das Modell benachrichtigt seine abhängigen Komponenten, wie die Views. Diese sind für die Darstellung der Daten zuständig, der Controller für die Benutzereingaben.
- Reaktor: Der Reaktor registriert die Ereignishändler. Der synchrone Event-Demultiplexer (
select
) benachrichtigt die Händler, wenn ein Ereignis eintritt.
Beiden Architekturmustern werde ich in Zukunft einen eigenen Artikel widmen.
Variationen
Das Subjekt im Programm observer.cpp
sendet in dem Beispiel eine Benachrichtigung. Gerne kommen fortgeschrittenere Arbeitsabläufe zum Einsatz:
Das Subjekt sendet
- einen Wert,
- eine Benachrichtigung, dass ein Wert verfügbar ist; danach muss der Beobachter ihn abholen,
- eine Benachrichtigung, einschließlich der Angabe, welcher Wert verfügbar ist. Der Beobachter holt diesen gegebenenfalls ab.
Verwandte Patterns
Das Mediator-Muster etabliert die Kommunikation zwischen einem Sender und seinem Empfänger. Jede Kommunikation zwischen den beiden Endpunkten läuft daher über diesen. Der Mediator und der Beobachter sind sich sehr ähnlich. Das Ziel des Mediators ist es, den Sender und den Empfänger zu entkoppeln. Im Gegensatz dazu stellt der Beobachter eine Einwegkommunikation zwischen dem Herausgeber und seinen Abonnenten her.
Vor- und Nachteile
Vorteile
- Neue Beobachter (Abonnenten) können einfach zum Observer (Herausgeber) hinzugefügt werden.
- Beobachter können sich zur Laufzeit selbst an- und abmelden.
Nachteile
- Der Verleger bietet weder eine Garantie, in welcher Reihenfolge die Abonnenten benachrichtigt werden, noch gibt er eine Aussage darüber an, wie lange die Benachrichtigung dauert, bis diese zugestellt ist.
- Es kann sein, dass der Herausgeber eine Benachrichtigung sendet, aber ein Abonnent nicht mehr am Leben ist. Um diesen Nachteil zu vermeiden, kann man den Destruktor der konkreten Beobachter so implementieren, dass sie sich in ihrem Destruktor selbst abmelden:
class ConcreteObserverA : public Observer {
public:
ConcreteObserverA(Subject& subject) : subject_(subject) {
subject_.registerObserver(this);
}
~ConcreteObserverA() noexcept {
subject_.unregisterObserver(this);
}
void notify() const override {
std::cout << "ConcreteObserverA::notify\n";
}
private:
Subject& subject_;
};
Der konkrete Beobachter ConcreteObserverA
setzt das RAII Idiom um: Er registriert sich in seinem Konstruktor und deregistriert sich in seinem Destruktor.
Wie geht's weiter?
Das Visitor-Muster hat einen zwiespältigen Ruf. Auf der einen Seite ermöglicht der Visitor Double Dispatch. Auf der anderen Seite ist der Visitor ziemlich kompliziert zu implementieren. Ich werde das Visitor Pattern in meinem nächsten Artikel genauer vorstellen. ()