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.

Lesezeit: 4 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 6 Beiträge
Fernglas

(Bild: TimmyTimTim/Shutterstock.com)

Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Das Beobachtermuster löst ein klassisches Designproblem: Wie kann man sicherstellen, dass alle Interessenten automatisch benachrichtigt werden, wenn ein wichtiges Ereignis stattgefunden hat?

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

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.

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.

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. ()