Mediator Pattern w C# – implementacja czatu pokoju
Kolejny wzorzec z serii opartej na Anatomii wzorca — 8 elementach opisu GoF. Tym razem Mediator — wzorzec, który ratuje kod, gdy N obiektów zaczyna znać wszystkie pozostałe N-1 obiektów.
1. Intent (Zamierzenie)
Definiuje obiekt, który hermetyzuje sposób, w jaki grupa obiektów ze sobą współpracuje. Mediator promuje luźne powiązanie, zapobiegając bezpośrednim referencjom obiektów do siebie nawzajem, i pozwala zmieniać ich interakcje niezależnie.
Krótko: zamiast N obiektów trzymających referencje do siebie nawzajem (relacja many-to-many), każdy zna tylko jednego mediatora (relacja many-to-one). Mediator koordynuje całą resztę.
2. Also Known As (Inne nazwy)
GoF nie podaje dla Mediatora popularnych alternatywnych nazw — w praktyce bywa nazywany po prostu “koordynatorem” albo, w kontekście UI, “dialog controller”.
3. Motivation (Motywacja)
GoF opisuje okno dialogowe z wieloma widgetami: listą, polem tekstowym, przyciskami. Zaznaczenie elementu na liście powinno wypełnić pole tekstowe i włączyć przycisk “Usuń”. Gdyby każdy widget bezpośrednio wywoływał metody pozostałych, dialog stałby się gęstą siatką wzajemnych referencji — a próba użycia tego samego widgetu w innym oknie byłaby niemożliwa, bo ciągnąłby za sobą całą sieć zależności.
Mediator centralizuje tę logikę koordynacji w jednym obiekcie (“dialog director”). Widgety znają tylko mediatora — nie wiedzą nawzajem o swoim istnieniu.
4. Applicability (Kiedy stosować)
Stosuj Mediator gdy:
- zestaw obiektów komunikuje się w złożony, ale dobrze określony sposób, a wynikające z tego zależności są chaotyczne i trudne do ogarnięcia,
- reużycie obiektu jest trudne, bo komunikuje się z wieloma innymi obiektami na sztywno,
- zachowanie rozproszone między kilka klas powinno być łatwe do zmiany bez tworzenia kolejnych podklas każdej z nich.
Sygnał ostrzegawczy w kodzie: klasa A wywołuje metody klasy B, B wywołuje metody C, C z powrotem wywołuje metody A — pajęczyna wzajemnych zależności, w której zmiana jednej klasy wymaga sprawdzenia wszystkich pozostałych.
5. Structure (Struktura)
┌──────────────────┐ ┌──────────────────┐
│ <<interface>> │ <────── │ <<interface>> │
│ Mediator │ │ Colleague │
│──────────────────│ │──────────────────│
│+Notify(sender,ev)│ │ +mediator │
└──────────────────┘ └──────────────────┘
△ △
│ ┌────────┴────────┐
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ConcreteMediator │ │ConcreteColleagueA│ │ConcreteColleagueB│
│──────────────────│ │──────────────────│ │──────────────────│
│ -colleagueA │ │ +DoA() │ │ +DoB() │
│ -colleagueB │ └──────────────────┘ └──────────────────┘
└──────────────────┘
ConcreteColleague obiekty nie znają się nawzajem — każdy zna tylko wspólny interfejs Mediator i wywołuje go, gdy chce coś zakomunikować reszcie. ConcreteMediator zna wszystkich uczestników i decyduje, kto ma zareagować na co.
6. Participants (Uczestnicy)
- Mediator — interfejs komunikacji między obiektami
Colleague. - ConcreteMediator — implementuje zachowanie kooperacyjne, koordynując swoich
Colleague. - Colleague — każdy obiekt komunikuje się ze swoim mediatorem zamiast bezpośrednio z innymi kolegami.
7. Collaborations (Współpraca)
Colleague wysyła i odbiera żądania za pośrednictwem obiektu Mediator. Mediator implementuje zachowanie kooperacyjne, kierując żądania do odpowiednich Colleague.
Colleague nigdy nie wywołuje metody innego Colleague bezpośrednio — zawsze przez mediatora, który decyduje, co dalej z tym żądaniem zrobić.
8. Consequences (Konsekwencje)
Plus: ogranicza tworzenie podklas — zmiana zachowania wymaga podklasowania tylko Mediatora, nie każdego z Colleague. Plus: odseparowuje Colleague od siebie nawzajem — zależą tylko od mediatora. Plus: upraszcza protokoły komunikacji — relację wiele-do-wielu zamienia w wiele-do-jednego. Minus: mediator może stać się nadmiernie złożony — scentralizowanie logiki w jednym miejscu grozi tym, że mediator sam stanie się trudną do utrzymania “klasą, która wie wszystko”.
Ten “minus” to realne ryzyko: dobrze zaprojektowany Mediator odciąża kolegów z logiki koordynacji, ale źle zaprojektowany po prostu przenosi cały bałagan w jedno miejsce, zamiast go uporządkować.
Mediator w C# — od diagramu do kodu
Scenariusz
Pokój czatu: użytkownicy wysyłają wiadomości, które mają trafić do wszystkich pozostałych. Bez wzorca każdy Uzytkownik musiałby trzymać listę referencji do wszystkich innych uczestników — i aktualizować ją przy każdym dołączeniu/wyjściu z pokoju.
Bez wzorca — użytkownicy znają się nawzajem
public class UzytkownikZly
{
public string Imie { get; }
private readonly List<UzytkownikZly> _pozostali = new(); // ❌ każdy zna wszystkich
public UzytkownikZly(string imie) => Imie = imie;
public void Dolacz(UzytkownikZly inny) => _pozostali.Add(inny);
public void WyslijWiadomosc(string tresc)
{
foreach (var u in _pozostali)
u.Odbierz(Imie, tresc); // każdy musi wiedzieć, jak zawiadomić resztę
}
public void Odbierz(string od, string tresc) => Console.WriteLine($"[{Imie}] {od}: {tresc}");
}
Dodanie nowego użytkownika wymaga zarejestrowania go u wszystkich pozostałych. Usunięcie kogoś z pokoju — odrejestrowania wszędzie. UzytkownikZly zna szczegóły komunikacji, które nie powinny go obchodzić.
Z wzorcem — ChatRoom jako mediator
public interface IChatMediator
{
void WyslijWiadomosc(string od, string tresc);
void Dolacz(Uzytkownik uzytkownik);
}
public class ChatRoom : IChatMediator
{
private readonly List<Uzytkownik> _uczestnicy = new();
public void Dolacz(Uzytkownik uzytkownik) => _uczestnicy.Add(uzytkownik);
public void WyslijWiadomosc(string od, string tresc)
{
foreach (var u in _uczestnicy.Where(u => u.Imie != od))
u.Odbierz(od, tresc);
}
}
public class Uzytkownik
{
private readonly IChatMediator _mediator;
public string Imie { get; }
public Uzytkownik(string imie, IChatMediator mediator)
{
Imie = imie;
_mediator = mediator;
_mediator.Dolacz(this);
}
public void Wyslij(string tresc) => _mediator.WyslijWiadomosc(Imie, tresc);
public void Odbierz(string od, string tresc) => Console.WriteLine($"[{Imie}] {od}: {tresc}");
}
var pokoj = new ChatRoom();
var anna = new Uzytkownik("Anna", pokoj);
var kuba = new Uzytkownik("Kuba", pokoj);
anna.Wyslij("Cześć wszystkim!"); // Kuba dostaje wiadomość, Anna o Kubie nic nie wie
Uzytkownik zna tylko IChatMediator — nie ma pojęcia, ilu jest innych uczestników ani kim są. Dodanie trzeciego użytkownika to jedna linijka konstruktora, zero zmian w istniejących klasach.
Mediator kontra Observer — częste pomylenie
Oba wzorce rozprzęgają obiekty, ale rozwiązują inny problem:
| Observer | Mediator | |
|---|---|---|
| Kierunek | jeden nadawca → wielu odbiorców (broadcast) | wielu uczestników ↔ jeden koordynator (dwukierunkowo) |
| Cel | powiadomienie o zmianie stanu | koordynacja złożonej interakcji między wieloma obiektami |
| Kto zna kogo | subject nie zna obserwatorów | colleague zna mediatora, mediator zna wszystkich colleague |
W praktyce ChatRoom powyżej mógłby być zaimplementowany przez event (Observer) zamiast interfejsu Mediator — ale gdyby pokój musiał dodatkowo decydować, kto dostaje wiadomość (np. tylko użytkownicy w tym samym kanale, moderowanie treści, blokowanie spamerów), to już nie jest zwykłe powiadomienie — to logika koordynacji, czyli realny Mediator.
Pułapka: mediator jako God Object
public class SuperMediator : IChatMediator
{
// 40 metod: WyslijWiadomosc, ZbanujUzytkownika, WalidujTresc,
// ZapiszDoLogow, WyslijPowiadomieniePush, SprawdzUprawnienia...
}
Jeśli mediator zaczyna przejmować odpowiedzialności, które nie dotyczą koordynacji (walidacja, logowanie, uprawnienia), łamie SRP dokładnie tak samo, jak klasy, które miał rozprzęgać. Rozwiązanie: mediator koordynuje przepływ komunikatów, a konkretne zadania (walidacja, logowanie) deleguje do osobnych, wyspecjalizowanych serwisów — nie robi ich sam.
Podsumowanie
Mediator zamienia sieć wzajemnych referencji między obiektami w gwiazdę, gdzie każdy zna tylko centralny koordynator. Colleague nie muszą wiedzieć nawzajem o swoim istnieniu, co ułatwia ich reużycie i testowanie w izolacji. Uważaj na jedną pułapkę: mediator ma koordynować komunikację, nie stawać się magazynem całej logiki biznesowej systemu — inaczej sam zamieni się w problem, który miał rozwiązać.
🚀 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