Das Observer-Pattern: reagieren Sie, anstatt zu pullen

vg

Das Observer-Pattern ermöglicht es Objekten, automatisch auf Änderungen in anderen Objekten zu reagieren, ohne dass eine enge Kopplung erforderlich ist. Es handelt sich um ein Verhaltensmuster, das beispielhaft für das klassische Publisher-Abonnenten-Modell ist, bei dem ein „Subjekt“ Änderungen sendet und eine beliebige Anzahl von „Beobachtern“ diese Updates erhält.

Was ist das Observer-Pattern?

Das Observer-Entwurfspattern definiert eine Eins-zu-Viele-Beziehung zwischen Objekten, sodass bei einer Änderung des Zustands eines Objekts alle abhängigen Elemente benachrichtigt und automatisch aktualisiert werden. In der Praxis verwaltet ein Objekt (das Subjekt oder der Verleger) eine Liste interessierter abhängiger Objekte (Beobachter oder Abonnenten) und benachrichtigt sie, wenn ein bestimmtes Ereignis oder eine Zustandsänderung auftritt. Dies ermöglicht einen Veröffentlichungs-/Abonnementmechanismus: Das Subjekt veröffentlicht Aktualisierungen, und Beobachter abonnieren, um diese Aktualisierungen zu erhalten.

Die meisten UI-Frameworks und ereignisgesteuerten Systeme machen von dieser Idee starken Gebrauch. GUI-Elemente können z. B. auf das „Klick“-Ereignis einer Schaltfläche lauschen (die Schaltfläche ist das Subjekt, Listener sind Beobachter), oder ein Modellobjekt kann mehrere Ansichtskomponenten benachrichtigen, wenn sich seine Daten ändern. Das Observer-Pattern ist eines der ursprünglichen GoF-Designmuster (Gang of Four), das für seine lockere Kopplung und Flexibilität im Design gelobt wird.

Analogie aus der realen Welt

Um das Konzept zu verdeutlichen, verwenden wir eine Analogie aus der realen Welt. Stellen Sie sich ein Kontrollzentrum der Weltraummission als Subjekt und verschiedene Subsysteme als Beobachter vor. Der Missionskontrollcomputer überwacht kontinuierlich kritische Daten wie Sauerstoffgehalt, Treibstoffstatus oder Druck. Wann immer sich etwas Wichtiges ändert (z. B. wenn der Sauerstoffgehalt sinkt), werden sofort mehrere Subsysteme benachrichtigt: Das Lebenserhaltungssystem schaltet den Backup-Sauerstoff ein, ein Alarmsystem gibt Alarm und ein Status-Dashboard wird für Ingenieure aktualisiert.

Jedes Subsystem hatte die Warnungen der Missionskontrolle über Sauerstoffveränderungen abonniert. Die Missionskontrolle (Subjekt) muss nicht wissen, wie jedes Subsystem mit dem Alarm umgeht. Es überträgt einfach das Ereignis. Die Beobachter (Subsysteme) reagieren automatisch auf ihre eigene Art und Weise – einer startet eine Backup-Pumpe, ein anderer löst einen Alarm aus, ein anderer protokolliert die Daten. Dieses Setup spiegelt das Observer-Pattern wider: ein Publisher (Mission Control) und mehrere Abonnenten (Lebenserhaltung, Alarm, Dashboard), die reagieren, wenn der Publisher ein Update „pusht“.

In dieser Analogie könnte die Missionskontrolle alle Teilnehmer erneut benachrichtigen, wenn der Sauerstoffgehalt wieder ansteigt, und einige Beobachter könnten dann ihre Notfallmaßnahmen deaktivieren. Der Vorteil liegt auf der Hand: Jede Komponente bleibt mit den zentralen Daten synchronisiert, ohne sie ständig zu überprüfen. Die Subsysteme müssen nicht immer wieder fragen: „Gibt es jetzt ein Sauerstoff-Update?“; Sie warten einfach, bis es ihnen gesagt wird. Dieses effiziente Benachrichtigungssystem stellt sicher, dass jeder rechtzeitig auf Änderungen reagiert, ähnlich wie das Observer-Pattern verhindert, dass Änderungen des Objektzustands von anderen Teilen eines Programms unbemerkt bleiben.

(Beachten Sie, dass es sich bei dieser Analogie nicht um eine Eins-zu-Eins-Zuordnung zu Softwarekomponenten handelt, sondern um eine Eins-zu-Eins-Zuordnung zu Softwarekomponenten, die den Geist einfängt: Eine einzige Quelle veröffentlicht Ereignisse, viele Empfänger ergreifen Maßnahmen. Niemand spammt Anfragen oder ist in einer engen Schleife von Überprüfungen gefangen.)

Funktionsweise des Observer-Patterns

Observer-Pattern
Observer Design Pattern UML-Klassendiagramm — Observer Muster — Wikipedia

Im Kern umfasst das Observer-Pattern zwei Gruppen von Akteuren: Subjekte und Beobachter (manchmal auch als Verleger bzw. Abonnenten bezeichnet). Schauen wir uns ihre Rollen und die Zusammenarbeit an:

  • Betreff (Verlag): Dies ist das Objekt, das einen interessanten Zustand oder ein interessantes Ereignis aufweist. Es verwaltet eine Liste von Beobachtern und stellt Methoden bereit, mit denen Beobachter anhängen (abonnieren) oder trennen (abmelden) können. Wenn sich der Zustand des Betreffs ändert oder ein Ereignis eintritt, werden alle registrierten Beobachter benachrichtigt, in der Regel durch Aufrufen einer bestimmten Aktualisierungsmethode für jeden Beobachter. Das Subjekt muss keine Details über die Beobachter wissen – nur, dass sie einer erwarteten Schnittstelle entsprechen (z. B. wenn sie eine Methode haben). Seine einzige Aufgabe ist es, die Beobachter im Auge zu behalten und sie über Änderungen zu informieren.update()
  • Beobachter (Abonnent): Hierbei handelt es sich um eine Schnittstelle oder einen abstrakten Typ, der die update-Methode definiert (den Rückruf, den Subjekte bei Benachrichtigungen aufrufen). Konkrete Beobachterklassen implementieren diese Schnittstelle und definieren, was zu tun ist, wenn sie ein Update erhalten. Beobachter registrieren sich bei einem Antragsteller, um Aktualisierungen zu erhalten, und sie sollten in der Lage sein, die Registrierung aufzuheben, wenn sie diese Aktualisierungen nicht mehr benötigen. Bei einer Benachrichtigung fragt ein Beobachter in der Regel die betroffene Person nach Details ab oder verwendet die in der Benachrichtigung übergebenen Daten, um entsprechend zu reagieren.
  • Meldemechanismus: Wenn ein Ereignis eintritt, geht das Subjekt seine Liste der Beobachter durch und benachrichtigt jeden einzelnen. Dies kann synchron erfolgen (der Betreff ruft die Aktualisierungsmethode jedes Beobachters sofort nacheinander auf) oder asynchron (der Betreff plant Benachrichtigungen für einen anderen Thread oder eine andere Ereignisschleife). Die klassische Implementierung ist der Einfachheit halber oft synchron – das Subjekt iteriert einfach und ruft Beobachtermethoden auf. In komplexeren Szenarien oder Frameworks können Benachrichtigungen asynchron gesendet werden, um eine Verlangsamung des Betreffs zu vermeiden oder um Beobachtern zu ermöglichen, Daten in ihrem eigenen Tempo zu verarbeiten.
  • Datenaktualisierungen (Push vs. Pull): Ein zentrales Designmerkmal ist, wie viele Informationen das Subjekt an seine Beobachter weitergibt. Im Push-Modell sendet es konkrete Änderungsdaten direkt mit der Benachrichtigung – praktisch, aber potenziell ineffizient, wenn Beobachter unnötige Daten erhalten. Im Pull-Modell informiert das Subjekt nur über eine Änderung; die Beobachter holen sich die nötigen Details bei Bedarf selbst. Beide Modelle sind gängig – welches besser passt, hängt vom Kontext und den Anforderungen ab. Das zugrunde liegende Observer-Prinzip bleibt dabei identisch.

Im Code (in einer beliebigen Sprache) umfasst das Pattern in der Regel eine Subject-Klasse mit Methoden wie , und , und eine Observer-Schnittstelle mit einer Methode (häufig parametrisiert mit Aktualisierungsinformationen). Beobachter registrieren sich bei dem Subjekt, und das Subjekt ruft es auf, wenn es angebracht ist. Dieses Design stellt sicher, dass Beobachter automatisch mit den Zustandsänderungen des Motivs synchronisiert bleiben, ohne dass das Motiv eng in die Beobachterlogik integriert ist. Sie können neue Beobachtertypen hinzufügen, ohne den Betreff zu ändern, was ein großer Gewinn für das Open/Closed-Prinzip ist (der Betreff kann über neue Beobachter erweitert werden, ist aber für Änderungen geschlossen).attach(observer)detach(observer)notifyObservers()update()observer.update(...)

Bevor Sie in den Code eintauchen, ist ein wichtiger Aspekt, den Sie beachten sollten, dass Beobachter und Subjekte lose gekoppelt sind. Das Subjekt muss keine konkreten Klassen von Beobachtern kennen, sondern nur, dass sie sich an die Beobachterschnittstelle halten. Das bedeutet, dass Sie Beobachter zur Laufzeit unabhängig voneinander hinzufügen oder entfernen können. Die Beobachter wissen über das Subjekt Bescheid (sie registrieren sich in der Regel bei einer bestimmten Subjektinstanz), aber idealerweise nur durch eine Abstraktion (z. B. indem sie die Benutzeroberfläche des Subjekts kennen oder zumindest, dass es die Art von Subjekt ist, die ihnen wichtig ist). Diese Entkopplung macht das Pattern flexibel: Wir können an eine beliebige Anzahl von Beobachtern senden, und Beobachter können sogar während des Programmlaufs hinzugefügt oder entfernt werden.

Beispiel: Implementieren von Observer in Swift

Zur Veranschaulichung implementieren wir eine einfache Version des Observer-Patterns in Swift. Stellen Sie sich vor, wir haben einen Blog, der Artikel veröffentlicht, und Leser, die sich anmelden, um über neue Beiträge benachrichtigt zu werden. Wir erstellen ein Subjekt und ein (Beobachter-)Protokoll, an das sich die Leser halten:BlogSubscriber

// Observer protocol that subscribers must conform to.
protocol BlogSubscriber: AnyObject {
func update(newArticle title: String)
}

// Subject class (Publisher)
class Blog {
private var subscribers = [BlogSubscriber]() // list of observers

func subscribe(_ subscriber: BlogSubscriber) {
subscribers.append(subscriber)
}

func unsubscribe(_ subscriber: BlogSubscriber) {
// Remove subscriber from the list (if present)
subscribers.removeAll { $0 === subscriber }
}

func publish(article title: String) {
print("Blog: Publishing new article '\(title)'")
notifySubscribers(title)
}

private func notifySubscribers(_ title: String) {
for subscriber in subscribers {
subscriber.update(newArticle: title)
}
}
}

// Concrete Observer class
class Reader: BlogSubscriber {
let name: String
init(name: String) { self.name = name }

func update(newArticle title: String) {
print("\(name) was notified about a new article: \(title)")
// Reader could, for example, fetch the article or mark it as unread.
}
}

// Usage example:
let techBlog = Blog()
let alice = Reader(name: "Alice")
let bob = Reader(name: "Bob")

techBlog.subscribe(alice)
techBlog.subscribe(bob)

techBlog.publish(article: "Understanding the Observer Pattern")
// Output:
// Blog: Publishing new article 'Understanding the Observer Pattern'
// Alice was notified about a new article: Understanding the Observer Pattern
// Bob was notified about a new article: Understanding the Observer Pattern

techBlog.unsubscribe(bob) // Bob unsubscribes from further notifications

techBlog.publish(article: "Swift Observer Pattern in Practice")
// Output:
// Blog: Publishing new article 'Swift Observer Pattern in Practice'
// Alice was notified about a new article: Swift Observer Pattern in Practice

In diesem Code:

  • Blog ist das Thema. Es führt eine Liste von Beobachtern. Die und-Methoden ermöglichen es Lesern, Benachrichtigungen zu starten oder zu beenden.BlogSubscribersubscribeunsubscribe
  • BlogSubscriber ist ein Protokoll (beschränkt auf Klassentypen mit, so dass wir bei Bedarf schwache Referenzen verwenden können). Es bedarf einer Methode, die das Subjekt bei jeder Veröffentlichung eines neuen Artikels auf jeden Betrachter anspricht.AnyObjectupdate(newArticle:)
  • Reader ist ein konkreter Beobachter (Abonnent), der die Methode implementiert. In unserem Beispiel druckt es nur eine Nachricht, aber in einer echten App könnte es beispielsweise eine Benachrichtigung anzeigen oder den neuen Artikelinhalt abrufen.update
  • Wenn der Blog aufgerufen wird, druckt er ein Protokoll und ruft dann auf, wodurch alle aktuellen Abonnenten durchlaufen und die Updatemethode mit dem neuen Artikeltitel aufgerufen wird. Auf diese Weise werden Alice und Bob (Beobachter) sofort über das Ereignis informiert, sobald es eintritt.publish(article:)notifySubscribers
  • Wir zeigen, dass Beobachter sich abmelden können. Nachdem Bob sich abgemeldet hat, wird Alice durch die Veröffentlichung eines weiteren Artikels nur benachrichtigt. Dadurch wird verhindert, dass Bob Updates erhält, an denen er nicht mehr interessiert ist.

Diese Implementierung ist absichtlich einfach gehalten. In einem realen Swift-Szenario könnten Sie einen robusteren Ansatz verwenden, um potenzielle Speicherlecks zu vermeiden, z. B. indem Sie Abonnenten so speichern, dass sie nicht für immer am Leben bleiben (mehr dazu in Kürze), oder indem Sie das Combine-Framework von Swift nutzen, das einen integrierten Publish-Subscribe-Mechanismus bietet. Aber als didaktisches Beispiel zeigt dies die Essenz des Observer-Patterns in Aktion: Der Blog muss nichts über die Interna des Lesers wissen (nur, dass er eine Methode hat), und die Leser fragen den Blog nicht aktiv ab – sie reagieren einfach, wenn der Blog ein Update an sie sendet.update

Vor- und Vorteile des Observer-Patterns

Das Observer-Pattern bietet mehrere Vorteile:

  • Entkopplung von Komponenten: Das Subjekt und der Betrachter sind lose miteinander verbunden. Das Subjekt muss die konkrete Klasse der Beobachter nicht kennen (nur, dass sie einer Schnittstelle folgen), und Beobachter müssen nicht viel über das Subjekt wissen, abgesehen von der Tatsache, dass es beobachtet werden kann. Dies fördert einen modularen Aufbau, bei dem Komponenten isoliert entwickelt und verstanden werden können.
  • Dynamisches und flexibles Verhalten: Beobachter können zur Laufzeit einfach hinzugefügt oder entfernt werden. Das bedeutet, dass Sie dynamisch anpassen können, wer Ereignisse abhört. Wenn z. B. eine neue Protokollierungskomponente Ereignisse überwachen muss, kann sie sich on-the-fly abonnieren. Das System kann von null auf viele Beobachter skaliert werden, ohne dass sich der Code im Betreff ändert.
  • Automatische Updates: Beobachter bleiben automatisch mit dem Zustand des Motivs synchronisiert. Es sind keine zwingenden Überprüfungen oder Abfrageschleifen erforderlich. Dies führt zu saubererem Code, da die Logik „Wenn X sich ändert, mach Y“ von der Struktur des Musters und nicht von verstreuten Überprüfungen gehandhabt wird. Es ist einfacher, die Konsistenz zwischen verwandten Teilen eines Programms zu wahren. (Wenn sich das Modell z. B. in einem Modellansichtsszenario ändert, werden alle Ansichten, die dieses Modell widerspiegeln, sofort standardmäßig aktualisiert.)
  • Broadcast-Kommunikation: Ein Ereignis kann zu beliebig vielen Reaktionen führen. Diese One-to-Many-Broadcast-Funktion ist sehr leistungsfähig. Ohne Observer müssen Sie möglicherweise jede Komponente explizit aufrufen (was gegen das Open/Closed-Prinzip verstößt, wenn neue Komponenten hinzugefügt werden). Bei Observer ist es dem Subjekt egal, wie viele Observer existieren. Es gibt nur das Ereignis aus und alle Interessierten verstehen es. Es ist eine elegante Möglichkeit, mehrere Teile eines Systems ohne hartcodierte Abhängigkeiten zu benachrichtigen.
  • Fördert die Wiederverwendung und Trennung von Belangen: Da das Muster das, was passiert (Ereignisgenerierung), von dem, was reagiert (Ereignisbehandlung), entkoppelt, können Sie das Subjekt in verschiedenen Kontexten mit unterschiedlichen Beobachtern oder Beobachter mit unterschiedlichen Subjekten wiederverwenden (vorausgesetzt, die Benutzeroberfläche stimmt überein). Die Sorge um das Generieren eines Ereignisses ist von der Sorge um die Verwendung des Ereignisses getrennt. Dies führt oft zu Code, der sich an die SOLID-Prinzipien hält (insbesondere die Open/Closed- und Single Responsibility-Prinzipien).

Fazit

Das Observer-Pattern ist eine zeitlose Lösung für Szenarien, in denen Änderungen in einem Teil eines Systems an andere Teile kommuniziert werden müssen. Durch die Einführung eines Publish-Subscribe-Modells auf Objektebene wird die Kopplung reduziert und Ihr Design flexibler und reaktionsschneller. Wir haben gesehen, wie es einem Subjekt ermöglicht, Ereignisse an eine beliebige Anzahl interessierter Beobachter zu senden, und wie dies einer fest codierten Logik oder kontinuierlicher Abfrage vorzuziehen ist. Wir haben auch erörtert, wann es angemessen ist, dieses Pattern zu verwenden, anstatt sich auf ausgefeiltere Ereignissysteme oder reaktive Bibliotheken zu verlassen. Das Verständnis von Observer hilft Ihnen nicht nur dabei, solche Muster selbst zu schreiben, sondern auch ereignisgesteuerte Frameworks in verschiedenen Sprachen (von UI-Callbacks bis hin zu vollständigen reaktiven Streams) zu verwenden und zu erkennen, die auf denselben Grundlagen basieren.

Beachten Sie bei der Verwendung von Observer die bewährten Methoden für die Verwaltung von Abonnements und die Vermeidung von Speicherverlusten. Stellen Sie sicher, dass Ihre Beobachter ihre Begrüßung nicht überziehen, und achten Sie auf die Leistung, wenn Sie eine sehr große Anzahl von Beobachtern oder sehr häufig benachrichtigen. Verwenden Sie das Pattern mit Bedacht – es eignet sich hervorragend für bestimmte Probleme, aber nicht jede Kommunikation in einem Programm muss eine Beobachterbeziehung sein.

In der Toolbox der Entwurfsmuster sticht Observer als derjenige hervor, der ein Netz von If-else-Überprüfungen oder direkten Aufrufen in ein sauberes Benachrichtigungssystem verwandelt. Neben Mustern wie „State“ (aus unserer vorherigen Diskussion), die bei der Verwaltung des Objektverhaltens helfen, hilft Observer bei der Verwaltung von Objektinteraktionen. Zusammen bieten diese Muster (und andere in der Reihe) bewährte Möglichkeiten, saubereren, besser wartbarer Code zu schreiben. Wenn Sie weiterhin auf komplexe Szenarien im Softwaredesign stoßen, denken Sie an das Observer-Pattern als eine Option für den eleganten Umgang mit Änderungen und Benachrichtigungen – Ihr zukünftiges Ich (und Ihre Mitarbeiter) werden es Ihnen danken, wenn der Code mit Anmut skaliert.

Observer Pattern

Ist das Observer Pattern speichereffizient genug für Mikrocontroller?

Ja, wenn es korrekt umgesetzt wird. Statt dynamischer Speicherallokation kann man statische Arrays und feste Puffergrößen verwenden. In ressourcenbeschränkten Embedded-Systemen ist eine einfache und deterministische Implementierung entscheidend – und genau das ermöglicht das Observer Pattern.

Observer Pattern vs. Event Queue – was ist besser im Embedded-System?

Vermeide das Observer Pattern, wenn: Die Anzahl der Observer zur Laufzeit stark variiert. Echtzeit-Determinismus oberste Priorität hat. Ressourcen extrem knapp sind.In solchen Fällen sind direkte Funktionsaufrufe oder Event Queues mit statischer Allokation oft sinnvoller.

Warum sollte man das Observer Pattern im Embedded-System verwenden?

Das Observer Pattern erlaubt eine lose Kopplung zwischen Softwarekomponenten. Das ist besonders wichtig in Embedded-Systemen, in denen Code modular, testbar und wartbar sein muss. Statt direkte Funktionsaufrufe zu verwenden, ermöglicht das Observer Pattern eine saubere Trennung zwischen Ereignisquelle und Ereignisverarbeitung.

Was ist das Observer Pattern in der Embedded-Programmierung?

Das Observer Pattern ist ein Entwurfsmuster, bei dem ein Subjekt (z. B. ein Sensor oder Ereignisgenerator) mehrere Observer (Beobachter) benachrichtigt, sobald sich ein Zustand ändert. In Embedded-Systemen wird das Observer Pattern oft verwendet, um Module zu entkoppeln und flexible Event-Handling-Mechanismen zu schaffen – ohne direkte Abhängigkeiten zwischen Komponenten.

Welche Risiken oder Nachteile hat das Observer Pattern im Embedded-Bereich?

Speicherverbrauch bei vielen Observern Race Conditions bei gleichzeitigen Updates Komplexität bei der Verwaltung dynamischer Observer Fehlende Debug-Möglichkeiten, da die Kommunikation indirekt erfolgt Daher ist eine sorgfältige Planung der Architektur essenziell, vor allem in sicherheitskritischen oder zeitkritischen Anwendungen.

Welche Vorteile bietet das Observer Pattern in Echtzeitbetriebssystemen (RTOS)?

Das Observer Pattern kann im RTOS-Kontext für eventgetriebene Kommunikation zwischen Tasks oder Modulen verwendet werden. Es reduziert die Notwendigkeit von Polling, verbessert die CPU-Effizienz und unterstützt die Entwicklung von reaktiven Systemarchitekturen.

Wie kann man das Observer Pattern in sicherheitskritischen Embedded-Systemen einsetzen?

In sicherheitskritischen Systemen (z. B. Automotive oder Medizintechnik) muss das Observer Pattern deterministisch und vorhersehbar implementiert werden. Das heißt: Keine dynamische Speicherallokation Feste Anzahl Observer Prüfung auf gültige Zeiger Klare Priorisierung und Ablaufsteuerung

Weiterer lesenswerte Beiträge: Wie Uber Milliarden von Transaktionen abwickelt – das Geheimnis und Neues von STM zum Thema STM32 05/2025

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)