🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Observer Pattern w C# – implementacja przez event

Jeśli znasz już Anatomię wzorca — 8 elementów opisu GoF, wiesz, że każdy z 23 wzorców GoF opisuje się w tym samym formacie. Rozkładamy w nim Observer — jeden z najczęściej używanych wzorców behawioralnych, wbudowany w C# pod postacią słowa kluczowego event.

1. Intent (Zamierzenie)

Definiuje zależność jeden-do-wielu między obiektami, tak że gdy jeden obiekt zmienia stan, wszyscy jego zależni są automatycznie powiadamiani i aktualizowani.

Krótko: jeden nadawca zmiany, wielu niezależnych odbiorców, którzy dowiadują się o zmianie bez pytania.

2. Also Known As (Inne nazwy)

Dependents (zależni), Publish-Subscribe — choć Publish-Subscribe w architekturze rozproszonej (kolejki wiadomości między serwisami) to już inna skala tego samego pomysłu, nie to samo narzędzie co Observer w jednym procesie.

3. Motivation (Motywacja)

Klasyczny przykład z GoF: arkusz kalkulacyjny i wykres słupkowy prezentują te same dane na dwa różne sposoby. Zmiana danych w arkuszu musi odświeżyć wykres — ale arkusz nie powinien nic wiedzieć o istnieniu wykresu, bo jutro może dojść tabela przestawna albo eksport do PDF.

Gdyby Arkusz znał konkretną klasę WykresSlupkowy i wywoływał jej metody bezpośrednio, każdy nowy sposób prezentacji danych wymagałby edycji Arkusza. Observer odwraca tę zależność: Arkusz (subject) zna tylko wspólny interfejs obserwatora, nie konkretne klasy.

4. Applicability (Kiedy stosować)

Stosuj Observer gdy:

  • zmiana jednego obiektu wymaga zmiany innych, ale nie wiesz z góry ilu i jakich,
  • obiekt powinien móc powiadamiać inne obiekty, nie zakładając nic o tym, czym one są,
  • masz dwa aspekty tego samego problemu (dane i ich prezentacja), które chcesz zmieniać i reużywać niezależnie od siebie.

Sygnał ostrzegawczy w kodzie: klasa domenowa (np. OrderService) bezpośrednio wywołuje metody serwisów, które logicznie nie mają nic wspólnego z jej główną odpowiedzialnością — wysyłka e-maili, logowanie do analytics, aktualizacja cache’u.

5. Structure (Struktura)

┌──────────────────┐         ┌──────────────────┐
│     Subject      │ ──────> │  <<interface>>   │
│──────────────────│         │     Observer     │
│ -observers[]     │         │──────────────────│
│──────────────────│         │ +Update()        │
│ +Attach(Observer)│         └──────────────────┘
│ +Detach(Observer)│                 △
│ +Notify()        │                   │
└──────────────────┘          ┌────────┴────────┐
         △                   │                  │
         │           ┌─────────────────┐ ┌─────────────────┐
┌──────────────────┐ │ConcreteObserverA│ │ConcreteObserverB│
│ ConcreteSubject  │ │─────────────────│ │─────────────────│
│──────────────────│ │ +Update()       │ │ +Update()       │
│ -state           │ └─────────────────┘ └─────────────────┘
└──────────────────┘

Subject trzyma listę obiektów implementujących Observer i wywołuje Update() na każdym z nich, gdy jego stan się zmieni. ConcreteSubject przechowuje realny stan; ConcreteObserver reaguje na zmianę, zwykle odpytując ConcreteSubject o aktualną wartość.

6. Participants (Uczestnicy)

  • Subject — zna swoich obserwatorów (dowolna ich liczba), udostępnia interfejs do subskrybowania i wypisywania się.
  • Observer — wspólny interfejs dla wszystkich obiektów, które mają być powiadamiane o zmianie.
  • ConcreteSubject — przechowuje stan interesujący ConcreteObserver, wysyła powiadomienie, gdy stan się zmienia.
  • ConcreteObserver — implementuje Update(), utrzymuje spójność swojego stanu ze stanem ConcreteSubject.

7. Collaborations (Współpraca)

ConcreteSubject powiadamia swoich obserwatorów za każdym razem, gdy zmiana mogłaby doprowadzić do niespójności między jego stanem a stanem obserwatorów. Po otrzymaniu powiadomienia, obserwator może odpytać subjecta, żeby zsynchronizować swój stan.

Czyli: subject nie wysyła obserwatorom pełnych danych na siłę — czasem samo powiadomienie “coś się zmieniło” wystarczy, a obserwator sam decyduje, czy i jakich danych potrzebuje.

8. Consequences (Konsekwencje)

Plus: abstrakcyjne powiązanie między Subject a Observer — subject nie zna konkretnych klas obserwatorów. Plus: wsparcie dla komunikacji rozgłoszeniowej (broadcast) do dowolnej liczby zainteresowanych obiektów. Minus: nieoczekiwane aktualizacje — obserwatorzy nie wiedzą nawzajem o sobie, więc pozornie niewinna zmiana w subject może wywołać kaskadę aktualizacji, której pełnego kosztu nie widać z miejsca, gdzie zmiana się zaczyna.

Ten “minus” to realny problem w dużych systemach: dodanie dziesiątego obserwatora do często zmienianego subjectu potrafi niezauważalnie spowolnić całą operację — każdy Notify() czeka teraz na dziesięć handlerów.


Observer w C# — od diagramu do kodu

Scenariusz

Zamówienie zmienia status na “Wysłane”. Trzeba wysłać e-mail do klienta, zaktualizować statystyki sprzedaży i powiadomić magazyn.

Bez wzorca — OrderService zna wszystkich odbiorców bezpośrednio:

public class OrderServiceZly
{
    private readonly EmailService _email = new();
    private readonly AnalyticsService _analytics = new();
    private readonly WarehouseService _warehouse = new();

    public void ZmienStatus(Order order, string nowyStatus)
    {
        order.Status = nowyStatus;
        _email.WyslijPotwierdzenie(order);       // ❌ OrderService musi znać EmailService
        _analytics.ZarejestrujZmiane(order);      // ❌ i AnalyticsService
        _warehouse.ZaktualizujStan(order);        // ❌ i WarehouseService
    }
}

Dodanie czwartego odbiorcy (np. powiadomień push) wymaga edycji OrderService — łamie to Open/Closed Principle.

Z wzorcem — klasyczny interfejs

public interface IOrderObserver
{
    void OnStatusChanged(Order order, string nowyStatus);
}

public class OrderService
{
    private readonly List<IOrderObserver> _observers = new();

    public void Subskrybuj(IOrderObserver observer) => _observers.Add(observer);

    public void ZmienStatus(Order order, string nowyStatus)
    {
        order.Status = nowyStatus;
        foreach (var observer in _observers)
        {
            observer.OnStatusChanged(order, nowyStatus);
        }
    }
}

public class EmailObserver : IOrderObserver
{
    public void OnStatusChanged(Order order, string nowyStatus)
        => Console.WriteLine($"Wysyłam e-mail: zamówienie {order.Id} → {nowyStatus}");
}
var service = new OrderService();
service.Subskrybuj(new EmailObserver());
service.Subskrybuj(new AnalyticsObserver());

service.ZmienStatus(order, "Wysłane"); // oba obserwatory reagują, OrderService o nich nie wie

Nowy obserwator to nowa klasa i jedna linijka Subskrybuj() — zero zmian w OrderService.

Idiomatyczny C# — Observer to event

C# ma ten wzorzec wbudowany w język. To dokładnie ten sam Observer, tylko bez ręcznie pisanej listy i interfejsu:

public class OrderService
{
    public event Action<Order, string>? StatusChanged;

    public void ZmienStatus(Order order, string nowyStatus)
    {
        order.Status = nowyStatus;
        StatusChanged?.Invoke(order, nowyStatus);
    }
}

var service = new OrderService();
service.StatusChanged += (order, status) => Console.WriteLine($"E-mail: {order.Id} → {status}");
service.StatusChanged += (order, status) => Console.WriteLine($"Analytics: {order.Id} → {status}");

service.ZmienStatus(order, "Wysłane");

Klasyczny interfejs IOrderObserver ma sens, gdy obserwator potrzebuje więcej niż jednej metody callback albo gdy zależy ci na jawnej, testowalnej abstrakcji. Do prostych powiadomień w jednym procesie event wystarcza w zupełności.

IObservable<T> i IObserver<T> — formalny wariant z .NET

Framework ma też gotowy kontrakt w przestrzeni nazw SystemIObservable<T> i IObserver<T>. W przeciwieństwie do eventIObserver<T> ma trzy metody — OnNextOnErrorOnCompleted — modeluje cały cykl życia strumienia zdarzeń, nie pojedyncze zdarzenie. To kontrakt, na którym opiera się Rx.NET — jeśli łączysz, filtrujesz albo opóźniasz strumienie zdarzeń, to Observer Pattern w wersji przemysłowej.

Pułapka: event trzyma referencję i wycieka pamięć

public class Raport
{
    public Raport(OrderService service)
    {
        service.StatusChanged += Obsluz; // ❌ brak odsubskrybowania
    }
    private void Obsluz(Order order, string status) { /* ... */ }
}

Dopóki OrderService żyje (singleton, kontener DI — często cały czas działania aplikacji), trzyma referencję do każdego subskrybenta, który się nie wypisał. GC nie może zwolnić Raport. Poprawka: zawsze odsubskrybuj w Dispose():

public class Raport : IDisposable
{
    private readonly OrderService _service;
    public Raport(OrderService service)
    {
        _service = service;
        _service.StatusChanged += Obsluz;
    }
    private void Obsluz(Order order, string status) { /* ... */ }
    public void Dispose() => _service.StatusChanged -= Obsluz; // ✅
}

Pułapka: jeden wybuchający obserwator blokuje resztę

Jeśli pierwszy subskrybent rzuci wyjątek, StatusChanged?.Invoke(...) przerywa wywoływanie kolejnych. W kodzie, gdzie obserwatory nie powinny na siebie wpływać, iteruj po liście delegatów ręcznie i izoluj błędy:

foreach (Action<Order, string> handler in StatusChanged?.GetInvocationList() ?? Array.Empty<Delegate>())
{
    try { handler(order, nowyStatus); }
    catch (Exception ex) { logger.LogError(ex, "Błąd obserwatora"); }
}

Testowanie kodu z Observer

public class FakeObserver : IOrderObserver
{
    public bool Wywolany { get; private set; }
    public void OnStatusChanged(Order order, string nowyStatus) => Wywolany = true;
}

[Fact]
public void ZmienStatus_PowiadamiaWszystkichObserwatorow()
{
    var service = new OrderService();
    var fake = new FakeObserver();
    service.Subskrybuj(fake);

    service.ZmienStatus(new Order(), "Wysłane");

    Assert.True(fake.Wywolany); // bez SMTP, bez bazy danych
}

Kiedy NIE stosować Observer Pattern

  • Jeden, stały odbiorca — zwykłe bezpośrednie wywołanie metody jest prostsze niż event z jednym subskrybentem na stałe.
  • Odbiorcy muszą wykonać się w gwarantowanej kolejności, z wynikiem wpływającym na dalszy przepływ — do sekwencyjnego potoku kroków lepiej pasuje Chain of Responsibility.
  • Powiadomienie musi przekroczyć granicę procesu — event działa tylko w jednym procesie w pamięci; do komunikacji między serwisami potrzebujesz kolejki wiadomości, nie C#-owego Observer.

Podsumowanie

Observer odwraca zależność między nadawcą zmiany a jej odbiorcami — subject zna tylko wspólny kontrakt, nie konkretne klasy. W C# realizujesz go najczęściej przez event, a do bardziej złożonych strumieni zdarzeń masz IObservable<T>. Dwie realne pułapki: zapomniane odsubskrybowanie (wyciek pamięci) i wyjątek w jednym handlerze blokujący resztę. To narzędzie do powiadomień w obrębie jednego procesu — nie do komunikacji między serwisami.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

1 comment

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →