Verhaltensmuster

Observer

Ein Vorgang hat Folgen, die den Vorgang selbst nichts angehen: Eine Bestellung wird angelegt, daraufhin soll eine Bestätigung raus, ein Lagerbestand sinken, eine Kennzahl steigen. Baut man das direkt ein, kennt die Bestellung all diese Empfänger. Beim Observer meldet sich stattdessen an, wer reagieren will.

Struktur

Das Subjekt hält eine Liste angemeldeter Beobachter und benachrichtigt sie bei Änderung. In der Praxis übernimmt diese Rolle meist ein Event-Dispatcher des Frameworks statt einer selbstgeschriebenen Liste.

final class BestellungAngelegt
{
    public function __construct(public readonly BestellNummer $nummer) {}
}

final class BestellungService
{
    public function __construct(private readonly EventDispatcher $events) {}

    public function anlegen(Warenkorb $korb): BestellNummer
    {
        $nummer = $this->repository->speichern(Bestellung::aus($korb));
        $this->events->dispatch(new BestellungAngelegt($nummer));

        return $nummer;
    }
}

Praxisnotiz

Observer ist das Muster mit der größten Spreizung zwischen „hilft sehr" und „kostet später viel". Die Entkopplung ist echt: Ein neuer Empfänger ist eine neue Klasse, der auslösende Code bleibt unberührt. Genau deshalb wächst die Zahl der Empfänger auch unbemerkt.

Was im Architektur-Review regelmäßig auffällt: Niemand kann mehr sagen, was beim Anlegen einer Bestellung eigentlich passiert. Die Antwort steht nicht im Code, sondern in der Summe aller Anmeldungen — verteilt über das Projekt. Bei der Fehlersuche heißt das, erst die Empfängerliste zu rekonstruieren, bevor man überhaupt anfangen kann. Ein gepflegtes Verzeichnis der Ereignisse und ihrer Abnehmer kostet wenig und spart genau diese Stunden.

Die zweite Frage, die früh geklärt gehört: synchron oder asynchron. Synchrone Listener laufen in derselben Transaktion — ein Fehler im Nebeneffekt rollt den Hauptvorgang zurück, was selten gewollt ist. Asynchrone Verarbeitung löst das, bringt aber Zustellgarantien, Reihenfolge und Wiederholungen als neue Themen mit. Diese Entscheidung fällt zu oft implizit, indem man nimmt, was das Framework gerade anbietet.

Drittens die Namensgebung: Ereignisse beschreiben Vergangenes (BestellungAngelegt), keine Aufträge (SendeBestaetigung). Ein Ereignis, das wie ein Befehl heißt, ist ein verkappter Methodenaufruf mit mehr Infrastruktur — dann ist der direkte Aufruf ehrlicher.

Wann es nicht passt

  • Wenn genau ein Empfänger existiert und keiner dazukommt. Der direkte Aufruf ist lesbarer und debugbar.

  • Wenn die Reaktion zum fachlichen Erfolg gehört. Was passieren muss, damit der Vorgang gültig ist, gehört in den Vorgang — nicht in einen Nebeneffekt.

  • Wenn Reihenfolge zwischen Empfängern nötig wird. Das ist ein Signal, dass hier ein Ablauf modelliert gehört, kein Ereignis.

Alle Design-Patterns