CodeGym /Kurse /C# SELF /Pattern "Beobachter" ( Obse...

Pattern "Beobachter" ( Observer)

C# SELF
Level 53, Lektion 3
Verfügbar

1. Einführung

Stell dir vor, du hast ein Objekt, das irgendwelche Aktionen ausführt — z.B. einen Button oder unseren Worker. Gleichzeitig gibt es viele andere Objekte, die auf diese Aktionen reagieren müssen. Wenn du im Worker-Klassen hartkodiert alle möglichen "Listener" pflegst, wird die Wartung zum Albtraum: jede Änderung in der Abonnentenliste erfordert Änderungen direkt im Worker.

Das verletzt das Open-Closed-Prinzip (OCP) und ist schlechte Architekturpraxis.

Pattern Observer: Grundidee

Das Pattern "Beobachter" (Observer) löst dieses Problem. Es erlaubt einem Publisher, eine beliebige Anzahl interessierter Listener über Änderungen zu informieren, ohne zu wissen, wer diese Listener sind und was sie tun. Der Publisher ruft einfach eine Benachrichtigung aus, und die Interessierten reagieren, wie sie wollen.

Analogie: Ein Newsletter-Abo. Die Redaktion oder der Kanal (Publisher) schickt eine neue Nachricht, und alle Abonnenten (Beobachter) bekommen sie. Die Redaktion muss nicht wissen, wer alle diese Leute sind — und das ist auch nicht nötig.

Interessante Tatsache: "Observer" ist so populär, dass es offiziell zur Gang of Four (GoF) Sammlung von Patterns gehört.

Observer in C#: Umsetzung mit Events und Delegates

In C# ist das Pattern "Observer" quasi "out of the box" durch Events und Delegates umgesetzt. Ein Event ist ein "Erweiterungspunkt", an den verschiedene Handler gebunden werden können. Statt manuell eine Abonnentenliste zu pflegen, übernimmt das Sprachfeature Events das. Unten sehen wir zuerst die "manuelle" Implementierung und dann die Event-basierte Variante.

2. Klassische Implementierung des Observer ohne Events

Schauen wir, wie das aussehen würde, wenn es keine Events in der Sprache gäbe:


// Schnittstelle des Beobachters
public interface IObserver
{
    void Update(string message);
}

// Publisher
public class Worker
{
    private List<IObserver> observers = new List<IObserver>();

    public void Subscribe(IObserver observer)
    {
        observers.Add(observer);
    }

    public void Unsubscribe(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void DoWork()
    {
        Console.WriteLine("Worker arbeitet...");
        NotifyObservers("Arbeit abgeschlossen!");
    }

    private void NotifyObservers(string message)
    {
        foreach (var observer in observers)
        {
            observer.Update(message);
        }
    }
}

// Konkreter Beobachter
public class WorkListener : IObserver
{
    public void Update(string message)
    {
        Console.WriteLine($"
WorkListener hat Nachricht erhalten: {message}");
    }
}

Initialisierung:


var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();

Hinweis: Hier wird die Abonnentenliste (List<IObserver> observers) manuell gepflegt, und Subscribe/Unsubscribe sind explizite Methoden.

3. Events und Delegates — die "high-level" Implementierung des Observer

Das gleiche lässt sich einfacher und eleganter mit Events implementieren. Das ist der C#-Stil des Observer:


public class Worker
{
    public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;

    public void DoWork()
    {
        Console.WriteLine("Worker arbeitet...");
        OnWorkCompleted("Arbeit abgeschlossen!");
    }

    protected virtual void OnWorkCompleted(string message)
    {
        WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
    }
}

public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; set; }
}

public class WorkListener
{
    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine($"WorkListener hat Nachricht erhalten: {e.Message}");
    }
}

// Subscription:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();

Vorteile dieses Ansatzes:

  • Kein manuelles Pflegen der Abonnentenliste nötig.
  • Alle Event-Möglichkeiten stehen zur Verfügung: multiple Subscriptions, Unsubscribe, Lambdas.
  • Sicherheit: nur der Publisher kann das Event auslösen.
  • Lose Kopplung: der Publisher weiß nichts über die Listener.

4. Wie der "Beobachter" in unsere Anwendung passt

Integrieren wir das Pattern Observer in unsere Konsolenanwendung. Der Worker kann beliebig viele Handler haben, die unterschiedlich auf das Beenden der Arbeit reagieren: jemand schreibt in die Konsole, jemand zählt die ausgeführten Arbeiten, jemand sendet eine Nachricht "Chef! Alles erledigt!".

Code mit zusätzlichen Beispielen


// Zweiter Listener-Zähler
public class WorkCounter
{
    public int Count { get; private set; }

    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Count++;
        Console.WriteLine($"Arbeit vermerkt. Insgesamt: {Count} erledigt.");
    }
}

// Objekte erstellen
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();

// Beide Subscriptions
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;

// Simulation mehrerer Arbeiten
worker.DoWork();
worker.DoWork();
// Output:
// Worker arbeitet...
// WorkListener hat Nachricht erhalten: Arbeit abgeschlossen!
// Arbeit vermerkt. Insgesamt: 1 erledigt.
// Worker arbeitet...
// WorkListener hat Nachricht erhalten: Arbeit abgeschlossen!
// Arbeit vermerkt. Insgesamt: 2 erledigt.

So fügst du Beobachter hinzu, ohne eine einzige Zeile im Worker-Code zu ändern. Die Klasse Worker bleibt unverändert, und das Systemverhalten erweitert sich über die Abonnenten.

5. Nützliche Nuancen

Echtes Beispiel: Observer in Interfaces und GUI

Das Observer-Pattern ist die Basis aller GUI-Frameworks. In Windows Forms oder WPF löst ein Button-Klick das Event Click aus. Du schreibst Handler (Beobachter), die auf dieses Event reagieren — weder deine Button-Klasse noch die .NET-Bibliothek müssen etwas über deine Abonnenten wissen.


// In WPF oder WinForms (ungefähr)
myButton.Click += (s, e) => MessageBox.Show("Benutzer hat auf den Button geklickt!");

Observer in realen Projekten

  • User Interfaces (Reaktion auf Klicks, Änderungen, Timer usw.).
  • Benachrichtigungs- und Event-Systeme.
  • Plugins für erweiterbare Systeme (Core feuert Events, Extensions abonnieren).
  • Verteilte Systeme und Game Engines (lose gekoppelte reaktive Ketten).

Kurz gesagt: Wenn du ein erweiterbares System brauchst, in dem Teile auf Änderungen anderer Teile reagieren können — Observer must have!

7. Besonderheiten, typische Fehler und deren Vermeidung

Potentielle Schwierigkeiten bei der Nutzung des Observer

Memory leaks. Wenn ein Subscriber sich auf ein Event registriert, aber nicht abmeldet (besonders bei lang lebenden Objekten), kann der Garbage Collector dieses Objekt nicht freigeben, weil der Publisher weiterhin eine Referenz über den Event-Delegate hält. Das ist kritisch, wenn der Subscriber nicht mehr gebraucht wird, der Publisher aber weiterlebt.

Mehrfache Subscription. Wenn derselbe Handler mehrfach registriert ist, wird er auch mehrfach aufgerufen — das führt zu doppelten Aktionen und unerwartetem Verhalten.

Exceptions in Handlern. Wenn einer der Handler eine Exception wirft, kann die Ausführung der folgenden Subscriber unterbrochen werden. Denke über Robustheit der Handler nach und, falls nötig, rufe sie in einem try-catch manuell auf, damit die anderen Subscriber trotzdem ausgeführt werden.

Häufiges Leaks-Schema

flowchart LR
    Publisher["Publisher
(Worker)"] -- event --> ObserverA["Listener A (Lebendig!)"] Publisher -- event --> ObserverB["Listener B (Leak: vergaß sich abzumelden)"]
2
Aufgabe
C# SELF, Level 53, Lektion 3
Gesperrt
Verwendung von Events zur Implementierung des Observer-Musters
Verwendung von Events zur Implementierung des Observer-Musters
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION