zurück zum Artikel

Patterns in der Softwareentwicklung: Das Beobachtermuster

Rainer Grimm
Fernglas

(Bild: TimmyTimTim/Shutterstock.com)

Das Beobachtermuster definiert 1-zu-n-Abhängigkeiten zwischen Objekten, sodass Änderungen an einem Objekt Benachrichtigungen der abhängigen Objekte anstoßen.

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 [1]". 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

Auch bekannt als

Anwendungsfall

Struktur

Subject

Observer

ConcreteObserver

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.

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

Verwandte Patterns

Das Mediator-Muster [2] 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

Nachteile

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 [3]Idiom um: Er registriert sich in seinem Konstruktor und deregistriert sich in seinem Destruktor.

Das Visitor-Muster [4] hat einen zwiespältigen Ruf. Auf der einen Seite ermöglicht der Visitor Double Dispatch [5]. Auf der anderen Seite ist der Visitor ziemlich kompliziert zu implementieren. Ich werde das Visitor Pattern in meinem nächsten Artikel genauer vorstellen. ( [6])


URL dieses Artikels:
https://www.heise.de/-7346525

Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Design_Patterns
[2] https://en.wikipedia.org/wiki/Mediator_pattern
[3] https://en.cppreference.com/w/cpp/language/raii
[4] https://en.wikipedia.org/wiki/Visitor_pattern
[5] https://en.wikipedia.org/wiki/Double_dispatch
[6] mailto:rainer@grimm-jaud.de