Skip to content

Das Observer Pattern in JavaScript - Vorteile, Nachteile und Praxisbeispiele

Published: at 07:00 AMSuggest Changes

Die Softwareentwicklung mit JavaScript wird zunehmend komplexer. Zahlreiche Komponenten, Module und Services müssen in Echtzeit miteinander kommunizieren und auf Events reagieren. Genau hier kann das Observer Pattern seine Stärken ausspielen: Es entkoppelt Sender von Empfängern, indem es eine lose geknüpfte Benachrichtigungsstruktur etabliert. So kann ein Teil der Anwendung (das sogenannte Subject) Veränderungen oder Events auslösen und andere, interessierte Teile (die Observer) halten sich stets auf dem aktuellen Stand, ohne dass eine starre, direkte Kopplung entsteht.

In diesem Artikel erfährst Du zunächst das Grundprinzip des Observer Patterns und wie es in JavaScript typischerweise umgesetzt wird. Anschließend beleuchten wir an praxisnahen Beispielen, wie Du das Muster im Alltag einsetzt, zum Beispiel in einer Chat-Anwendung oder bei UI-Updates. Danach gehen wir auf Vor- und Nachteile ein, um Dir eine fundierte Entscheidungsgrundlage zu geben, ob dieses Entwurfsmuster Deinen Anforderungen entspricht. Abschließend ziehen wir ein Fazit und beantworten in einem FAQ-Teil häufig gestellte Fragen rund um das Thema.


1. Grundprinzipien des Observer Patterns

Beim Observer Pattern werden in der Regel zwei Rollen definiert. Das Subject (oder Publisher) und einen oder mehrere Observer (auch Subscriber genannt). Das Subject veröffentlicht Benachrichtigungen, sobald sich sein Zustand ändert. Die Observer haben sich zuvor beim Subject registriert und erhalten automatisch eine Meldung, wenn etwas Relevantes passiert ist.

1.1. Typischer Ablauf

  1. Das Subject besitzt eine Liste an Observern, die informiert werden sollen.
  2. Über eine Methode wie addObserver oder subscribe melden sich Observer an.
  3. Eine Änderung tritt im Subject auf. Zum Beispiel wurde ein Wert aktualisiert, eine externe Datenquelle hat neue Informationen oder ein Timer ist abgelaufen.
  4. Das Subject ruft eine Benachrichtigungsmethode wie notify auf. In diesem Schritt werden alle registrierten Observer über den Event informiert.
  5. Die Observer führen eine eigene Methode – zum Beispiel update – aus und reagieren darauf, indem sie etwa den angezeigten UI-Inhalt aktualisieren oder Daten neu anfordern.

Die lose Kopplung entsteht dadurch, dass das Subject nicht wissen muss, was die Observer tatsächlich tun. Es ruft nur eine vereinbarte Funktion (update, onNotify o. Ä.) auf und übergibt bei Bedarf relevante Daten.

1.2. Einfaches Beispiel zur Veranschaulichung

class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class ConcreteObserver {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`*Observer* "${this.name}" wurde benachrichtigt. Daten:`, data);
  }
}

// Nutzung
const subject = new Subject();
const observerA = new ConcreteObserver('A');
const observerB = new ConcreteObserver('B');

subject.addObserver(observerA);
subject.addObserver(observerB);

subject.notify({ message: 'Hallo Welt!' });
// *Observer* "A" wurde benachrichtigt. Daten: { message: 'Hallo Welt!' }
// *Observer* "B" wurde benachrichtigt. Daten: { message: 'Hallo Welt!' }

Das Codebeispiel zeigt die Implementierung des Observer Patterns in JavaScript. Die Klasse Subject repräsentiert das Subjekt, das beobachtet wird. Im Konstruktor (constructor) wird ein Array observers initialisiert, das die Liste der Observer enthält. Die Methode addObserver fügt einen Observer zur Liste hinzu, während die Methode removeObserver einen Observer aus der Liste entfernt. Die Methode notify durchläuft alle Observer und ruft deren update-Methode auf, um sie über neue Daten zu informieren.

Die Klasse ConcreteObserver repräsentiert einen konkreten Observer. Jeder Observer hat einen Namen, der im Konstruktor (constructor) gesetzt wird. Die update-Methode wird aufgerufen, wenn das Subjekt eine Benachrichtigung sendet, und gibt die empfangenen Daten in der Konsole aus.

Im Nutzungsteil des Codes wird ein Subject-Objekt erstellt und zwei ConcreteObserver-Objekte (observerA und observerB) instanziiert. Diese Observer werden dem Subjekt hinzugefügt. Wenn die notify-Methode des Subjekts aufgerufen wird, werden beide Observer benachrichtigt und geben die Nachricht “Hallo Welt!” in der Konsole aus.

Dieses Beispiel zeigt, wie das Observern Pattern verwendet werden kann, um eine lose Kopplung zwischen einem Subjekt und seinen Observern zu erreichen, sodass Änderungen im Subjekt automatisch an die Observer weitergegeben werden.


2. Praxisnahe Beispiele

Um das Observer Pattern in echten Projekten anzuwenden, sind häufige Anwendungsfälle zum Beispiel UI-Komponenten, Chat-Systeme, Daten-Streams oder die Synchronisation unterschiedlicher Modulzustände. Im Folgenden siehst Du zwei praxisnahe Beispiele.

2.1. UI-Komponenten aktualisieren (Frontend)

Stell Dir eine Webanwendung vor, in der mehrere Komponenten dieselbe Datenquelle anzeigen müssen. Sobald die Datenquelle neue Informationen hat, sollen alle Komponenten sofort aktualisiert werden, ohne dass eine Komponente explizit weiß, welche anderen Komponenten existieren.

class Store {
  constructor() {
    this.state = { counter: 0 };
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unSubscribe(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

  notify() {
    this.observers.forEach(observer => observer.update(this.state));
  }

  increment() {
    this.state.counter++;
    this.notify();
  }
}

class CounterDisplay {
  constructor(elementId) {
    this.element = document.getElementById(elementId);
  }

  update(state) {
    this.element.textContent = `Aktueller Zähler: ${state.counter}`;
  }
}

// Beispielhafter Einsatz im Browser
const store = new Store();
const displayA = new CounterDisplay('counterA');
const displayB = new CounterDisplay('counterB');

// *Observer* registrieren
store.subscribe(displayA);
store.subscribe(displayB);

// Aktion auslösen
document.getElementById('button').addEventListener('click', () => {
  store.increment();
});

Der Store repräsentiert hier das Subject. Die Komponenten (CounterDisplay) fungieren als Observer und rufen in der update-Methode jene Logik auf, die den DOM aktualisiert. Wenn die Nutzer:in den Button klickt und store.increment() auslöst, werden alle registrierten Observer automatisch aufgerufen.

Das aktive Codebeispiel zeigt die Implementierung des Observern Pattern in einem Browser-Kontext mit JavaScript. Das Observern Pattern ermöglicht es, eine Eins-zu-viele-Abhängigkeit zwischen Objekten zu erstellen, sodass wenn ein Objekt seinen Zustand ändert, alle abhängigen Objekte benachrichtigt und automatisch aktualisiert werden.

Die Klasse Store repräsentiert das Subjekt, das beobachtet wird. Im Konstruktor (constructor) wird der anfängliche Zustand state mit einem Zähler (counter) auf 0 gesetzt und ein Array observers initialisiert, das die Liste der Observer enthält. Die Methode subscribe fügt einen Observer zur Liste hinzu. Die Methode notify durchläuft alle Observer und ruft deren update-Methode auf, um sie über den aktuellen Zustand zu informieren. Die Methode increment erhöht den Zähler um eins und benachrichtigt anschließend alle Observer.

Die Klasse CounterDisplay repräsentiert einen konkreten Observer. Im Konstruktor (constructor) wird ein HTML-Element anhand seiner ID (elementId) referenziert. Die update-Methode wird aufgerufen, wenn das Subjekt eine Benachrichtigung sendet, und aktualisiert den Textinhalt des HTML-Elements mit dem aktuellen Zählerstand.

Im Nutzungsteil des Codes wird ein Store-Objekt erstellt und zwei CounterDisplay-Objekte (displayA und displayB) instanziiert, die jeweils auf unterschiedliche HTML-Elemente verweisen. Diese Observer werden dem Store-Objekt hinzugefügt. Ein Klick-Event-Listener wird auf ein HTML-Element mit der ID button gesetzt, der bei jedem Klick die increment-Methode des Store-Objekts aufruft.

Dieses Beispiel zeigt, wie das Observern Pattern verwendet werden kann, um eine lose Kopplung zwischen einem Subjekt und seinen Observern zu erreichen, sodass Änderungen im Subjekt automatisch an die Observer weitergegeben werden und diese ihre Darstellung entsprechend aktualisieren.

2.2. Chat-Anwendung (Backend oder Frontend)

Betrachten wir in unserem zweiten Praxisbeispiel eine Chat-Anwendung, bei der mehrere Komponenten gleichzeitig über neue Nachrichten informiert werden sollen. Ein Chat-Service kann als Subject dienen, Observer könnten eine UI-Komponente oder ein Logging-System sein.

class ChatService {
  constructor() {
    this.messages = [];
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

  notifyAll(newMessage) {
    this.observers.forEach(observer => observer.update(newMessage));
  }

  sendMessage(user, text) {
    const message = { user, text, timestamp: Date.now() };
    this.messages.push(message);
    // Alle *Observer*:innen informieren
    this.notifyAll(message);
  }
}

class ChatUI {
  update(message) {
    console.log(`Neue Nachricht von ${message.user}: ${message.text}`);
    // UI-spezifisches Rendering, z.B. in einem Chat-Fenster
  }
}

class MessageLogger {
  update(message) {
    // Beispiel: Speichere in Datei oder in einer Datenbank
    console.log('Log: ', JSON.stringify(message));
  }
}

// Nutzung
const chatService = new ChatService();
chatService.addObserver(new ChatUI());
chatService.addObserver(new MessageLogger());

// Neue Nachricht wird gesendet
chatService.sendMessage('Alice', 'Hallo zusammen!');
// Neue Nachricht von Alice: Hallo zusammen!
// Log: {"user":"Alice","text":"Hallo zusammen!","timestamp":1678973456789}

Hier zeigt sich anschaulich, wie lose gekoppelt das System bleiben kann. Der ChatService weiß nicht, was seine Observer genau tun, er sagt ihnen nur: „Hier ist eine neue Nachricht, macht damit, was Ihr wollt!“.

Die Klasse ChatService repräsentiert das Subjekt, das beobachtet wird. Im Konstruktor (constructor) wird ein Array messages initialisiert, das die gesendeten Nachrichten speichert, und ein Array observers, das die Liste der Observer enthält. Die Methode addObserver fügt einen Observer zur Liste hinzu. Die Methode notifyAll durchläuft alle Observer und ruft deren update-Methode auf, um sie über eine neue Nachricht zu informieren. Die Methode sendMessage erstellt eine neue Nachricht mit dem Benutzer, dem Text und einem Zeitstempel, fügt diese Nachricht dem messages-Array hinzu und benachrichtigt anschließend alle Observer.

Die Klasse ChatUI repräsentiert einen konkreten Observer, der für die Benutzeroberfläche zuständig ist. Deren update-Methode wird aufgerufen, wenn das Subjekt eine Benachrichtigung sendet, und gibt die neue Nachricht in der Konsole aus. In einer realen Anwendung würde diese Methode die Nachricht in einem Chat-Fenster anzeigen.

Die Klasse MessageLogger repräsentiert einen weiteren konkreten Observer, der für das Protokollieren von Nachrichten zuständig ist. Deren update-Methode wird aufgerufen, wenn das Subjekt eine Benachrichtigung sendet, und protokolliert die Nachricht in der Konsole. In einer realen Anwendung könnte diese Methode die Nachricht in einer Datei oder einer Datenbank speichern.

Im Nutzungsteil des Codes wird ein ChatService-Objekt erstellt und zwei Observer (ChatUI und MessageLogger) hinzugefügt. Wenn die sendMessage-Methode des ChatService-Objekts aufgerufen wird, wird eine neue Nachricht erstellt und alle Observer werden benachrichtigt, sodass die Nachricht in der Konsole angezeigt und protokolliert wird.

Dieses Beispiel zeigt, wie das Observern Pattern verwendet werden kann, um eine lose Kopplung zwischen einem Subjekt und seinen Observern zu erreichen, sodass Änderungen im Subjekt automatisch an die Observer weitergegeben werden und diese entsprechend reagieren können. Was die Observer mit diesen Daten anfangen, ist dem Subject dabei völlig egal.


3. Vor- und Nachteile des Observer Patterns

3.1. Vorteile

Eine der größten Stärken liegt in der losen Kopplung, die langfristig die Wartbarkeit fördert. Das Subject bleibt minimalistisch und hält lediglich eine Liste von Observern. Neue Observer lassen sich jederzeit hinzufügen, ohne dass bestehender Code im Subject geändert werden muss. Außerdem passt das Pattern hervorragend zu reaktiven Architekturen, bei denen Zustandsänderungen sofort weitergereicht werden sollen, ohne eine starre, synchrone Abhängigkeit zu erzwingen.

Ein weiterer Vorteil ist die klare Trennung von Zustandsverwaltung und Darstellung oder Verarbeitungslogik. Der Zustand liegt im Subject, während Observer entweder UI-Updates oder spezielle Abläufe umsetzen, die nur durch ein bestimmtes Ereignis getriggert werden. Dadurch wird das System gut testbar, da Du das Subject getrennt von den Observern prüfen kannst und nur sicherstellen musst, dass die Observer die richtigen Benachrichtigungen erhalten.

3.2. Nachteile und mögliche Gefahren

Wo viele lose gekoppelte Bestandteile zusammenkommen, besteht immer die Gefahr, den Überblick zu verlieren. Wenn eine Anwendung sehr viele Observer und unterschiedliche Events aufweist, kann das Debuggen unübersichtlich werden. Besonders in größeren Projekten muss man gut dokumentieren, welche Observer sich registrieren und welche Aktionen sie bei Benachrichtigungen ausführen.

Zudem ist eine inflationäre Nutzung des Patterns potenziell performancelastig, wenn ständig Ereignisse ausgelöst und zahlreiche Observer benachrichtigt werden. In vielen Projekten bleibt das allerdings überschaubar, solange Du nur jene Komponenten Beobachter sein lässt, die wirklich auf Updates reagieren müssen.

Ein weniger beachteter, aber in der Praxis kritischer Punkt sind Memory Leaks. Diese können vor allem auftreten, wenn Observer sich am Subject registrieren, aber niemals wieder entfernt werden. So kann es geschehen, dass Objekte nicht vom Garbage Collector freigegeben werden, obwohl sie gar nicht mehr benötigt werden. Insbesondere in Single-Page-Applications, wo häufig Komponenten an- und abgemeldet werden, ist das eine reale Gefahr. Vergisst Du zum Beispiel, einen Listener in einer React-Komponente bei der Deinstallation zu entfernen, bleibt das Observer-Objekt im Speicher „hängen“ und kann mit der Zeit zu einem wachsenden Speicherverbrauch führen.

Ein kleines Beispiel für ein potenzielles Memory Leak:

// Potentieller "Leak", wenn Observers nicht entfernt werden
function attachObserver(subject) {
  const observer = {
    update(data) {
      console.log('Observer immer noch registriert:', data);
    }
  };
  subject.addObserver(observer);
}

// Nutzungsbeispiel
const subject = new Subject();
attachObserver(subject);

// Falls "observer" nie wieder entfernt wird, 
// bleibt es im subject.observers[] für die komplette Laufzeit
subject.notify('Beispieldaten');

Hier sollte man sicherstellen, dass beim Unmount oder bei einem Szenario, wo der Observer nicht mehr benötigt wird, die removeObserver-Methode aufgerufen wird, um das Objekt aus dem Array zu entfernen. Damit wird verhindert, dass der Observer noch Aktualisierungen erhält und dadurch unnötig Ressourcen verbraucht.


4. Fazit

Das Observer Pattern ist in JavaScript ein äußerst hilfreiches und flexibel anwendbares Entwurfsmuster. Es unterstützt dabei, lose gekoppelte Architekturen zu erschaffen, in denen sich Komponenten gegenseitig über wichtige Events informieren. Das fördert die Wartbarkeit und ermöglicht es, neue Funktionalitäten ohne großen Umbau hinzuzufügen. Dennoch sollte man das Pattern gezielt und bedacht einsetzen, da eine zu ausufernde Nutzung schnell zu unübersichtlichen Strukturen führt. In überschaubarem Umfang jedoch erleichtert das Observer Pattern den Umgang mit reaktiven Datenflüssen und bietet Dir, gerade in Verbindung mit modernen JavaScript-Frameworks oder Bibliotheken, eine saubere und gut testbare Lösung.


FAQ – 10 häufig gestellte Fragen zum Observer Pattern in JavaScript

1. Was ist der Unterschied zwischen dem Observer Pattern und Publish/Subscribe?
Der Unterschied liegt in der Implementierung. Beim Observer Pattern registrieren sich Observer direkt beim Subjekt und werden von diesem benachrichtigt. Beim Publish/Subscribe-Ansatz existiert eine vermittelnde Instanz (z.B. ein Event-Bus), über die Events gepublished und von Subscribern entgegengenommen werden. Ein minimales Pub/Sub in JavaScript kann zum Beispiel so aussehen:

const eventBus = {
  listeners: {},
  subscribe(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  },
  publish(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(cb => cb(data));
    }
  }
};

Das Codebeispiel zeigt die Implementierung eines einfachen Event-Bus-Systems in JavaScript. Ein Event-Bus ermöglicht die Kommunikation zwischen verschiedenen Teilen einer Anwendung durch das Veröffentlichen und Abonnieren von Events.

Das eventBus-Objekt enthält ein listeners-Objekt, das die registrierten Events und deren zugehörige Rückruffunktionen speichert. Die Methode subscribe ermöglicht es, eine Rückruffunktion für ein bestimmtes Event zu registrieren. Wenn das Event noch nicht existiert, wird ein neues Array für dieses Event erstellt. Die Rückruffunktion wird dann zu diesem Array hinzugefügt.

Die Methode publish ermöglicht es, einen Event zu veröffentlichen und Daten an alle registrierten Rückruffunktionen zu übergeben. Wenn das Event existiert, wird jede Rückruffunktion im Array aufgerufen und die übergebenen Daten werden an die Rückruffunktion weitergeleitet.

Dieses Event-Bus-System ermöglicht eine lose Kopplung zwischen verschiedenen Teilen der Anwendung, da Komponenten Events veröffentlichen und abonnieren können, ohne direkt miteinander verbunden zu sein. Dies fördert eine modulare und skalierbare Architektur.

2. Kann ich das Observer Pattern auch serverseitig in Node.js einsetzen?
Ja, Node.js liefert sogar mit dem EventEmitter ein fertiges Konzept, das sehr ähnlich zum Observer Pattern ist. Du kannst mit emitter.on('eventName', handler) Observer einrichten und mit emitter.emit('eventName', payload) Events auslösen.

const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('dataReceived', data => {
  console.log('Serverseitige Verarbeitung:', data);
});

emitter.emit('dataReceived', { info: 'Testdaten' });

Das Codebeispiel zeigt die Verwendung des EventEmitter-Moduls aus Node.js, um ein einfaches Event-Handling zu implementieren. Der EventEmitter ist ein Kernmodul von Node.js, das es ermöglicht, Events zu erstellen, zu abonnieren und auszulösen.

Zunächst wird das EventEmitter-Modul importiert und eine neue Instanz von EventEmitter erstellt, die in der Variablen emitter gespeichert wird. Diese Instanz dient als zentraler Punkt für das Verwalten von Events.

Die Methode on wird verwendet, um einen Listener für ein bestimmtes Event zu registrieren. In diesem Fall wird ein Listener für das Event dataReceived registriert. Wenn dieses Event ausgelöst wird, wird die angegebene Rückruffunktion aufgerufen, die die empfangenen Daten in der Konsole ausgibt.

Die Methode emit wird verwendet, um das Event dataReceived auszulösen und Daten an die registrierten Listener zu übergeben. In diesem Beispiel wird das Event mit einem Objekt { info: ‘Testdaten’ } ausgelöst, das dann von der Rückruffunktion verarbeitet wird.

Dieses Beispiel zeigt, wie der EventEmitter verwendet werden kann, um eine einfache und effektive event-basierte Kommunikation in einer Node.js-Anwendung zu implementieren. Dies ermöglicht eine lose Kopplung zwischen verschiedenen Teilen der Anwendung, da Komponenten Events auslösen und darauf reagieren können, ohne direkt miteinander verbunden zu sein.

3. Wie kann ich Observer gut testen?
Du könntest für jeden Observer prüfen, ob er die korrekten Daten bei der Benachrichtigung erhält. Mithilfe von Jest oder einer anderen Testbibliothek lässt sich prüfen, ob die update-Methode ordnungsgemäß aufgerufen wird:

test('Observer wird richtig benachrichtigt', () => {
  const subject = new Subject();
  const mockObserver = { update: jest.fn() };
  
  subject.addObserver(mockObserver);
  subject.notify('Payload');

  expect(mockObserver.update).toHaveBeenCalledWith('Payload');
});

Das Codebeispiel zeigt einen Unit-Test, der überprüft, ob ein Observer korrekt benachrichtigt wird, wenn das Subjekt (Subject) eine Benachrichtigung sendet. Der Test wird mit dem Test-Framework Jest geschrieben.

Der Test beginnt mit der Erstellung einer neuen Instanz von Subject. Anschließend wird ein Mock-Objekt mockObserver erstellt, das eine update-Methode enthält, die mit jest.fn() als Mock-Funktion definiert wird. Mock-Funktionen in Jest ermöglichen es, Aufrufe zu überwachen und zu überprüfen, ob sie wie erwartet ausgeführt wurden.

Der mockObserver wird dann dem subject als Observer hinzugefügt, indem die Methode addObserver aufgerufen wird. Danach wird die Methode notify des subject aufgerufen und der String ‘Payload’ als Argument übergeben. Dies soll die Benachrichtigung simulieren, die an alle registrierten Observer gesendet wird.

Der expect-Aufruf überprüft schließlich, ob die update-Methode des mockObserver mit dem Argument ‘Payload’ aufgerufen wurde. Die Methode toHaveBeenCalledWith von Jest stellt sicher, dass die update-Methode genau mit diesem Argument aufgerufen wurde.

Dieser Test stellt sicher, dass das Observern Pattern korrekt implementiert ist und dass Observer ordnungsgemäß benachrichtigt werden, wenn das Subjekt eine Benachrichtigung sendet. Dies ist wichtig, um die Integrität und Funktionalität des Systems zu gewährleisten.

4. Wie handhabe ich mehrere Event-Types in einem Subject?
Du kannst Dein Subject erweitern, sodass es unterschiedliche Event-Kanäle oder -Typen verwaltet. Damit können Observer sich nur für jene Events registrieren, die sie interessieren:

class MultiSubject {
  constructor() {
    this.events = {};
  }
  on(event, observer) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(observer);
  }
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(obs => obs.update(data));
    }
  }
}

Das Codebeispiel zeigt die Implementierung einer Klasse MultiSubject, die das Observern Pattern erweitert, um mehrere Events zu unterstützen. Diese Klasse ermöglicht es, verschiedene Arten von Events zu definieren und Observer für jedes spezifische Event zu registrieren und zu benachrichtigen.

Im Konstruktor wird ein Objekt events initialisiert, das die verschiedenen Events und deren zugehörige Observer speichert. Die Methode on ermöglicht es, einen Observer für ein bestimmtes Event zu registrieren. Wenn das Event noch nicht existiert, wird ein neues Array für dieses Event erstellt. Der Observer wird dann zu diesem Array hinzugefügt.

Die Methode emit ermöglicht es, einen Event auszulösen und Daten an alle registrierten Observer dieses Events zu übergeben. Wenn das Event existiert, wird jede Observer-function im Array aufgerufen und die übergebenen Daten werden an die update-Methode des Observers weitergeleitet.

Dieses erweiterte Observern Pattern ermöglicht ein flexibles und skalierbares Event-Handling, da verschiedene Events unabhängig voneinander behandelt werden können. Dies fördert eine modulare Architektur, in der Komponenten Events abonnieren und auslösen können, ohne direkt miteinander verbunden zu sein.

5. Ist das Observer Pattern dasselbe wie das Reactive Programming, zum Beispiel in RxJS?
Das Observer Pattern ist eine grundlegende Idee dahinter. RxJS implementiert Observables und Operators, wodurch komplexe Datenflüsse abgebildet werden können. Das grundsätzliche Prinzip, dass Publisher Daten senden und Subscriber darauf reagieren, ist jedoch vergleichbar. Ein kleines RxJS-Beispiel sähe so aus:

import { fromEvent } from 'rxjs';

const clicks$ = fromEvent(document, 'click');
clicks$.subscribe(event => console.log('Klick:', event));

Das Codebeispiel zeigt die Verwendung der Bibliothek RxJS (Reactive Extensions for JavaScript), um Events im Browser zu beobachten und darauf zu reagieren. RxJS ermöglicht die Arbeit mit asynchronen Datenströmen und Events auf eine deklarative Weise.

Zunächst wird die Funktion fromEvent aus der RxJS-Bibliothek importiert. Diese Funktion erstellt einen Observable-Stream aus einem DOM-Event. Im Beispiel wird ein Observable clicks$ erstellt, das alle Klick-Events (‘click’) auf dem document-Objekt beobachtet.

Die Methode subscribe wird verwendet, um einen Observer zu registrieren, der auf die Events im Observable-Stream reagiert. In diesem Fall wird eine anonyme Funktion als Observer registriert, die jedes Klick-Event empfängt und in der Konsole ausgibt. Jedes Mal, wenn ein Klick auf das Dokument erfolgt, wird die Nachricht “Klick:” gefolgt von den Event-Daten in der Konsole angezeigt.

Dieses Beispiel zeigt, wie RxJS verwendet werden kann, um Events im Browser zu beobachten und darauf zu reagieren. Durch die Verwendung von Observables und der subscribe-Methode wird eine reaktive Programmierung ermöglicht, die eine saubere und deklarative Handhabung von asynchronen Events und Datenströmen bietet.

6. Sollte man jede Datenquelle in einer Anwendung als Subject deklarieren?
Das hängt von Deinem Architekturansatz und Deinen Anforderungen ab. Oft ist es sinnvoll, zentrale Stores oder Datenquellen zu haben, die als Subjekte fungieren. Eine inflationäre Nutzung kann aber schnell unübersichtlich werden. Eine Devise lautet: Nur Daten, die wirklich mehrere Komponenten oder Services interessieren, sollten Observer bekommen.

7. Wie sieht ein funktionaler Ansatz ohne Klassen aus?
In JavaScript musst Du keine Klassen verwenden, um das Muster umzusetzen. Du kannst eine einfache Factory-Funktion mit Closures erstellen:

function createSubject() {
  let observers = [];
  return {
    addObserver(o) { observers.push(o); },
    notify(data) { observers.forEach(obs => obs(data)); }
  };
}

// Nutzung
const subject = createSubject();
subject.addObserver(msg => console.log('*Observer* empfängt:', msg));
subject.notify('Hello Closures!');

Das Codebeispiel zeigt die Implementierung eines einfachen Observern Pattern unter Verwendung von Closures in JavaScript. Closures ermöglichen es, eine Funktion zusammen mit ihrem umgebenden Zustand (den Lexical Environment) zu speichern, sodass auf Variablen aus dem äußeren Funktionskontext zugegriffen werden kann, auch nachdem die äußere Funktion beendet ist.

Die Funktion createSubject erstellt ein Subjekt, das beobachtet werden kann. Innerhalb dieser Funktion wird ein Array observers deklariert, das die Liste der Observer speichert. Die Funktion gibt ein Objekt zurück, das zwei Methoden enthält: addObserver und notify.

Die Methode addObserver fügt einen Observer zur Liste der Observer hinzu. Die Methode notify durchläuft alle Observer und ruft deren Funktionen mit den übergebenen Daten auf.

Im Nutzungsteil des Codes wird ein Subjekt durch Aufruf der Funktion createSubject erstellt. Ein Observer wird hinzugefügt, der eine anonyme Funktion ist, die eine Nachricht in der Konsole ausgibt. Schließlich wird die Methode notify des Subjekts aufgerufen und die Nachricht ‘Hello Closures!’ übergeben. Dies führt dazu, dass der Observer die Nachricht empfängt und in der Konsole ausgibt.

Dieses Beispiel zeigt, wie Closures verwendet werden können, um das Observern Pattern zu implementieren und den Zustand (die Liste der Observer) privat zu halten, während dennoch Methoden bereitgestellt werden, um diesen Zustand zu manipulieren.

8. Welche Rolle spielt „this“ bei Observern, wenn ich Arrow Functions verwende?
Arrow Functions haben keinen eigenen this-Kontext, sie binden stattdessen den this-Kontext des äußeren Bereichs. In den meisten Observer-Szenarien verwendest Du daher eine normale Methode oder Du arbeitest mit Arrow Functions in Verbindung mit einer externen Variable, wenn Du keinen Zugriff auf this benötigst. Wichtig ist lediglich, dass Du einheitlich bleibst und Dir klar darüber bist, wie der Kontext aussieht. Siehe hierzu auch noch mal hinter das Türchen meines letztjährigen JavaScript Adventskalenders zum Thema Arrow Functions.

9. Kann ich das Observer Pattern auch mit asynchronen Operationen kombinieren?
Ja, das ist sehr üblich. Du kannst zum Beispiel Observer erst nach Abschluss eines API-Calls benachrichtigen, sodass sie mit den geladenen Daten weiterarbeiten. Dein Subject könnte so aussehen:

class AsyncSubject {
  constructor() { this.observers = []; }

  addObserver(observer) { this.observers.push(observer); }

  async fetchData() {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    const data = await response.json();
    this.observers.forEach(o => o.update(data));
  }
}

Das Codebeispiel zeigt die Implementierung einer Klasse AsyncSubject, die das Observern Pattern verwendet, um asynchrone Datenabrufe zu handhaben und Observer über die empfangenen Daten zu informieren.

Im Konstruktor der Klasse AsyncSubject wird ein Array observers initialisiert, das die Liste der Observer speichert. Die Methode addObserver fügt einen Observer zur Liste hinzu. Ein Observer ist ein Objekt, das eine update -Methode implementiert, die aufgerufen wird, wenn neue Daten verfügbar sind.

Die Methode fetchData ist eine asynchrone Methode, die Daten von einer externen API abruft. In diesem Fall wird die API verwendet, um eine Liste von Posts abzurufen. Die Methode verwendet fetch, um die Daten abzurufen, und await, um auf die Antwort zu warten. Nachdem die Antwort in JSON umgewandelt wurde, durchläuft die Methode alle registrierten Observer und ruft deren update-Methode mit den empfangenen Daten auf.

Dieses Beispiel zeigt, wie das Observern Pattern verwendet werden kann, um asynchrone Datenabrufe zu handhaben und Observer automatisch zu benachrichtigen, wenn neue Daten verfügbar sind. Dies ermöglicht eine lose Kopplung zwischen dem Subjekt, das die Daten abruft, und den Observern, die auf die Daten reagieren, was zu einer modulareren und skalierbareren Architektur führt.

10. Wie integriere ich das Observer Pattern in Frameworks wie React oder Vue?
Viele moderne Frameworks besitzen eigene Mechanismen (z.B. React Hooks oder die Reaktivität von Vue). Du kannst das Observer Pattern dennoch ergänzend einsetzen, wenn Du unabhängige Logik-Module oder Services bauen möchtest, die andere Teile informieren. In React könntest Du etwa einen benutzerdefinierten Hook schreiben, der sich beim Subject anmeldet und dafür sorgt, dass ein State-Update stattfindet, sobald neue Daten vorliegen.

function useSubject(subject) {
  const [data, setData] = React.useState(null);
  
  React.useEffect(() => {
    const observer = { update: (newData) => setData(newData) };
    subject.addObserver(observer);
    return () => subject.removeObserver(observer);
  }, [subject]);

  return data;
}

Das Codebeispiel zeigt die Implementierung eines benutzerdefinierten React-Hooks namens useSubject, der das Observern Pattern in eine React-Komponente integriert. Dieser Hook ermöglicht es, den Zustand einer React-Komponente automatisch zu aktualisieren, wenn das Subjekt neue Daten bereitstellt.

Die Funktion useSubject nimmt ein subject als Argument entgegen. Innerhalb der Funktion wird der React-State-Hook useState verwendet, um eine Zustandsvariable data und eine Funktion setData zu initialisieren. Der anfängliche Zustand wird auf null gesetzt.

Der React-Effekt-Hook useEffect wird verwendet, um Seiteneffekte zu handhaben. In diesem Fall wird ein Observer-Objekt erstellt, das eine update-Methode enthält. Diese Methode setzt den Zustand der Komponente auf die neuen Daten, die vom Subjekt bereitgestellt werden. Der Observer wird dem Subjekt hinzugefügt, indem die Methode addObserver aufgerufen wird.

Der Rückgabewert der useEffect-Funktion ist eine Bereinigungsfunktion, die ausgeführt wird, wenn die Komponente unmontiert wird oder wenn sich das subject ändert. Diese Bereinigungsfunktion entfernt den Observer vom Subjekt, indem die Methode removeObserver aufgerufen wird.

Der Hook useSubject gibt die Zustandsvariable data zurück, die die aktuellen Daten des Subjekts enthält. Wenn das Subjekt neue Daten bereitstellt, wird der Zustand der Komponente automatisch aktualisiert, und die Komponente wird neu gerendert.

Dieses Beispiel zeigt, wie das Observern Pattern in eine React-Anwendung integriert werden kann, um eine reaktive und modulare Architektur zu schaffen, bei der Komponenten automatisch auf Änderungen im Zustand des Subjekts reagieren.

11. Kann ich WeakRefs verwenden, um die Gefahr von Memory-Leaks zu vermindern? Ja, du kannst grundsätzlich Memory-Leaks durch den Einsatz von WeakRefs reduzieren oder sogar ganz vermeiden, allerdings ist das kein Allheilmittel. In der Praxis ist die manuelle Deregistrierung von Observern (z.B. via removeObserver) nach wie vor der stabilste Ansatz, weil er sicherstellt, dass dein Code logisch „aufräumt“, sobald Beobachter:innen nicht mehr gebraucht werden. WeakRefs können dieses Aufräumen ergänzen oder in bestimmten Fällen automatisieren, sind jedoch etwas komplexer in der Handhabung und nicht in allen Situationen die beste Lösung.


Buy me a coffee

Wenn Dir meine Beiträge gefallen und sie Dir bei Deiner Arbeit helfen, würde ich mich über einen “Kaffee” und ein paar nette Worte von Dir freuen.

Buy me a coffee


JavaScript Closures: Ein vollständiger Leitfaden

Arrow Functions – Elegante Syntax für Funktionen
WeakMap und WeakSet – Speicher effizient verwalten


Previous Post
Das Prototype Pattern in JavaScript – Ein ausführlicher Leitfaden
Next Post
Das Factory Pattern in JavaScript - Vorteile, Nachteile und Praxisbeispiele