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 stanemConcreteSubject.
7. Collaborations (Współpraca)
ConcreteSubjectpowiadamia 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 System: IObservable<T> i IObserver<T>. W przeciwieństwie do event, IObserver<T> ma trzy metody — OnNext, OnError, OnCompleted — 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 —
eventdział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.
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
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ę →
1 comment