CodeGym /Kursy /C# SELF /Wzorzec "Obserwator" ( Obse...

Wzorzec "Obserwator" ( Observer)

C# SELF
Poziom 53, Lekcja 3
Dostępny

1. Wprowadzenie

Wyobraź sobie, że masz obiekt, który wykonuje jakieś akcje — na przykład przycisk albo nasz Worker. Istnieje przy tym wiele innych obiektów, które powinny reagować na te akcje. Jeśli w klasie Worker na twardo wpiszesz wszystkich możliwych „słuchaczy”, utrzymanie takiego kodu stanie się koszmarem: każda zmiana w liście subskrybentów będzie wymagać edycji samego Worker.

To łamie zasadę otwarte-zamknięte (OCP) i jest uznawane za kiepską praktykę architektoniczną.

Wzorzec Obserwator: ogólna idea

Wzorzec "Obserwator" (Observer) rozwiązuje ten problem. Pozwala obiektowi-wydawcy powiadamiać dowolną liczbę zainteresowanych obiektów-słuchaczy o zajściach, nie wiedząc nic o tym, kim są ci słuchacze i co robią. Wydawca po prostu „wywołuje alarm”, a chętni reagują według własnego widzimisię.

Analogia: subskrypcja newslettera. Redakcja lub kanał (wydawca) wysyła nową wiadomość, a wszyscy subskrybenci (obserwatorzy) ją otrzymują. Redakcja nie musi wiedzieć, kim są ci ludzie.

Ciekawostka: "Obserwator" jest tak popularny, że oficjalnie wchodzi w skład „bandy czterech” (GoF).

Observer w C#: wcielenie przez zdarzenia i delegaty

W C# wzorzec "Obserwator" jest zaimplementowany „z pudełka” przez mechanizmy zdarzeń i delegatów. Zdarzenie — to punkt rozszerzalności, do którego można się podpinać różnymi handlerami. Zamiast ręcznego trzymania listy subskrybentów, robi to mechanizm języka. Poniżej najpierw zobaczymy „ręczną” implementację, a potem wersję przez zdarzenia.

2. Klasyczna implementacja Observer bez użycia zdarzeń

Sprawdźmy, jak to mogłoby wyglądać, gdyby w języku nie było zdarzeń:


// Interfejs obserwatora
public interface IObserver
{
    void Update(string message);
}

// Wydawca
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 pracuje...");
        NotifyObservers("Praca zakończona!");
    }

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

// Konkretny obserwator
public class WorkListener : IObserver
{
    public void Update(string message)
    {
        Console.WriteLine($"WorkListener otrzymał wiadomość: {message}");
    }
}

Inicjalizacja:


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

Warto zauważyć: Tutaj lista subskrybentów (List<IObserver> observers) jest prowadzona ręcznie, a subskrypcja/odsubskrypcja — jawne metody Subscribe/Unsubscribe.

3. Zdarzenia i delegaty — "wysokopoziomowa" implementacja Observer

Możemy to samo zrobić prościej i ładniej za pomocą zdarzeń. To właśnie Observer w stylu C#:


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

    public void DoWork()
    {
        Console.WriteLine("Worker pracuje...");
        OnWorkCompleted("Praca zakończona!");
    }

    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 otrzymał wiadomość: {e.Message}");
    }
}

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

Plusy takiego podejścia:

  • Nie trzeba ręcznie utrzymywać listy subskrybentów.
  • Dostępne są wszystkie możliwości zdarzeń: wielokrotna subskrypcja, odsubskrypcja, lambdy.
  • Gwarantowane bezpieczeństwo: tylko wydawca może wywołać zdarzenie.
  • Luźne powiązanie: wydawca nic nie wie o słuchaczach.

4. Jak "obserwator" wpisuje się w naszą aplikację

Zintegrujmy wzorzec Observer w naszej aplikacji konsolowej. Niech Worker ma dowolną liczbę handlerów, które zareagują na zakończenie pracy na różne sposoby: ktoś wypisze do konsoli, ktoś policzy liczbę wykonanych zadań, ktoś wyśle wiadomość "Szef! Wszystko zrobione!".

Rozszerzamy kod przykładami


// Drugi słuchacz - licznik
public class WorkCounter
{
    public int Count { get; private set; }

    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Count++;
        Console.WriteLine($"Praca zaliczona. Razem: {Count} wykonanych.");
    }
}

// Tworzymy obiekty
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();

// Obie subskrypcje
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;

// Symulacja kilku prac
worker.DoWork();
worker.DoWork();
// Wyjście:
// Worker pracuje...
// WorkListener otrzymał wiadomość: Praca zakończona!
// Praca zaliczona. Razem: 1 wykonanych.
// Worker pracuje...
// WorkListener otrzymał wiadomość: Praca zakończona!
// Praca zaliczona. Razem: 2 wykonanych.

W ten sposób dodajesz "obserwatorów" w miarę potrzeby, nie zmieniając ani jednej linii w kodzie Worker. Klasa Worker pozostaje niezmieniona, a zachowanie systemu rozszerza się przez subskrybentów.

5. Przydatne niuanse

Praktyczny przykład: Observer w interfejsach i GUI

Wzorzec Observer to podstawa wszystkich frameworków GUI. W Windows Forms czy WPF kliknięcie przycisku wywołuje zdarzenie Click. Piszesz handlery (obserwatorów), które reagują na to zdarzenie — i ani twoja klasa Button, ani biblioteka .NET nie muszą nic wiedzieć o twoich subskrybentach.


// W WPF lub WinForms (przykład)
myButton.Click += (s, e) => MessageBox.Show("Użytkownik kliknął przycisk!");

Observer w realnych projektach

  • Interfejs użytkownika (reakcja na kliknięcia, zmiany, timery itp.).
  • Systemy powiadomień i eventów.
  • Pluginy do systemów rozszerzalnych (rdzeń generuje zdarzenia, rozszerzenia się subskrybują).
  • Systemy rozproszone i silniki gier (słabo powiązane łańcuchy reakcyjne).

Krótko mówiąc, jeśli chcesz zrobić rozszerzalny system, w którym jedne części mogą reagować na zmiany innych — Observer must have!

7. Cecha, typowe błędy i ich zapobieganie

Potencjalne trudności przy użyciu Obserwatora

Wycieki pamięci. Jeśli subskrybent zapisał się na zdarzenie, ale się nie wypisał (szczególnie w obiektach długożyjących), to garbage collector nie będzie w stanie go zwolnić, bo wydawca dalej trzyma referencję przez delegat zdarzenia. To krytyczne, jeśli subskrybent nie jest już potrzebny, a wydawca żyje dalej.

Wielokrotna subskrypcja. Jeśli ten sam handler subskrybuje się dwa razy, zostanie wywołany dwa razy — dostaniesz duplikaty akcji i niespodziewane efekty.

Wyjątki w handlerach. Jeśli jeden z handlerów rzuci wyjątek, wykonanie kolejnych subskrybentów może zostać przerwane. Przemyśl odporność handlerów i w razie potrzeby wywołuj je ręcznie w try-catch, żeby pozostali subskrybenci wykonali się mimo awarii jednego.

Typowa schematyczna przyczyna wycieków

flowchart LR
    Publisher["Wydawca
(Worker)"] -- zdarzenie --> ObserverA["Słuchacz A (Żyje!)"] Publisher -- zdarzenie --> ObserverB["Słuchacz B (Wyciekanie: zapomnieli odsubskrybować)"]
2
Zadanie
C# SELF, poziom 53, lekcja 3
Niedostępne
Wykorzystanie zdarzeń do implementacji Observer
Wykorzystanie zdarzeń do implementacji Observer
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION