Mediator
Cel
Mediator to behawioralny wzorzec projektowy pozwalający zredukować chaos zależności pomiędzy obiektami. Wzorzec ten ogranicza bezpośrednią komunikację pomiędzy obiektami i zmusza je do współpracy wyłącznie za pośrednictwem obiektu mediatora
Problem
Załóżmy, że masz okno dialogowe służące tworzeniu i edycji profili klientów. Składa się z różnych kontrolek, takich jak pola tekstowe, pola wyboru, przyciski, itd.
Niektóre elementy formularza mogą współdziałać z innymi. Przykładowo, zaznaczenie pola wyboru “Mam psa” spowoduje pojawienie się ukrytego wcześniej pola tekstowego do wpisywania imienia. Inny przykład to przycisk “Wyślij” który musi dokonać walidacji danych w polach zanim je zapisze.
Implementacja tej logiki bezpośrednio w kodzie elementów formularza sprawi, że klasy elementów będzie trudno ponownie użyć w innych formularzach. Przykładowo, nie będzie można użyć klasy pola wyboru w innym formularzu, ponieważ jest sprzężony z polem tekstowym imienia psa. Można albo użyć wszystkie klasy biorące udział w renderowaniu formularza profilu, albo nie używać żadnych.
Rozwiązanie
Wzorzec Mediator sugeruje przerwanie bezpośredniej komunikacji między komponentami które mają być niezależne. W zamian, komponenty te muszą współpracować pośrednio, wywołując specjalny obiekt mediatora, który przekierowuje wywołania do odpowiednich komponentów. W wyniku tego komponenty zależą tylko od pojedynczej klasy mediatora, zamiast sprzężenia ze sobą nawzajem.
W naszym przykładzie z formularzem edycji profilu, klasa okna dialogowego może pełnić rolę mediatora. Prawdopodobnie klasa ta wie już o swoich podelementach, więc nie trzeba nawet wprowadzać nowych zależności do tej klasy.
Najistotniejsza zmiana dotyczy samych elementów formularza. Rozważmy przycisk wysyłania. Wcześniej, za każdym razem gdy kliknięto przycisk, musiał on dokonać walidacji wartości we wszystkich elementach formularza. Teraz jego jedynym zadaniem jest powiadomienie okna dialogowego o kliknięciu. Otrzymawszy powiadomienie, okno dialogowe dokonuje walidacji lub przekazuje to zadanie poszczególnym elementom formularza. Tym samym, zamiast sprzężenia z wieloma elementami, przycisk zależy jedynie od klasy okna dialogowego.
Można pójść o krok dalej i rozluźnić powiązanie jeszcze bardziej, ekstrahując wspólny interfejs dla wszystkich typów okien dialogowych. Taki interfejs deklarowałby metodę powiadamiania, za pomocą której wszystkie elementy formularza mogłyby powiadamiać okno dialogowe o zdarzeniach z nimi związanych. Dzięki temu przycisk wysyłania będzie mógł współdziałać z dowolnym oknem dialogowym implementującym ten interfejs.
Jest to sposób w jaki wzorzec Mediator pozwala hermetyzować złożone plątaniny relacji pomiędzy obiektami w jednym obiekcie. Im mniej zależności ma klasa, tym łatwiej ją modyfikować, rozszerzać lub użyć ponownie.
Analogia do prawdziwego życia
Piloci statków powietrznych zbliżający się do lotniska lub je opuszczający nie rozmawiają ze sobą nawzajem, lecz za pośrednictwem kontrolera lotów, siedzącego w wieży z widokiem na lądowisko. Bez kontrolera, piloci musieliby wiedzieć o każdym samolocie lub śmigłowcu w okolicy lotniska, dyskutować na temat pierwszeństwa lądowania. Mogłoby to niekorzystnie wpłynąć na statystyki bezpieczeństwa...
Wieża nie musi kontrolować całego lotu. Jej funkcja służy nakładaniu ograniczeń w obszarze terminala aby żaden z pilotów nie był przytłoczony dużą ilością aktorów biorących udział w funkcjonowaniu lotniska.
Struktura
-
Komponenty to różne klasy zawierające jakąś logikę biznesową. Każdy komponent posiada odniesienie do mediatora, zadeklarowany jako typ interfejsu mediatora. Komponent ten nie jest świadom faktycznej klasy obiektu mediatora, więc można go używać ponownie w innych programach, łącząc z innym mediatorem.
-
Interfejs Mediator deklaruje metody komunikacji z komponentami, które na ogół ograniczają się do jednej metody powiadamiania. Komponenty mogą przekazywać dowolny kontekst jako argument tej metody, włącznie z samymi sobą, ale wyłącznie w sposób który nie spowoduje sprzęgnięcia komponentu otrzymującego z klasą nadawcy.
-
Konkretni Mediatorzy hermetyzują relacje pomiędzy różnorakimi komponentami. Konkretni mediatorzy często przechowują odniesienia do wszystkich komponentów jakimi zarządzają i czasem nawet zarządzają ich cyklem życia.
-
Komponenty nie mogą być świadome innych komponentów. Jeśli coś istotnego zdarzy się w innym komponencie, musi on powiadamiać wyłącznie mediatora. Gdy zaś ten otrzyma powiadomienie, może w prosty sposób zidentyfikować nadawcę i to czasem wystarcza, by zdecydować jaki komponent uruchomić w odpowiedzi.
Z perspektywy komponentu, wszystko przypomina czarną skrzynkę. Nadawca nie wie kto ostatecznie obsłuży jego żądanie, a odbiorca nie wie kto je nadał.
Pseudokod
W poniższym przykładzie, wzorzec Mediator pomaga wyeliminować wzajemne zależności pomiędzy różnymi klasami UI: przyciskami, polami wyboru i etykietami tekstowymi.
Element, uruchomiony przez użytkownika, nie komunikuje się bezpośrednio z innymi elementami, nawet jeśli wydaje się, że mógłby. W zamian element musi dać znać o zdarzeniu tylko swojemu mediatorowi, przekazując ewentualne dane kontekstowe wraz z powiadomieniem.
W tym przykładzie, całe okno dialogowe uwierzytelniania pełni rolę mediatora. Wie jak konkretne elementy powinny współpracować i zapewnia im pośrednią komunikację ze sobą. Otrzymawszy powiadomienie o zdarzeniu, okno dialogowe decyduje który element powinien zająć się jego obsługą i odpowiednio przekierowuje wywołanie.
Zastosowanie
Stosuj wzorzec Mediator gdy zmiana jakichś klas jest trudna z powodu ścisłego sprzęgnięcia z innymi klasami.
Wzorzec pozwala wyekstrahować wszystkie relacje pomiędzy klasami do osobnej klasy, izolując ewentualne zmiany określonego komponentu od reszty komponentów.
Stosuj ten wzorzec gdy nie możesz ponownie użyć jakiegoś komponentu w innym programie, z powodu zbytniej jego zależności od innych komponentów.
Po zastosowaniu wzorca Mediator, pojedyncze komponenty stają się nieświadome innych komponentów. Nadal mogą komunikować się ze sobą, ale pośrednio — poprzez obiekt mediator. Aby ponownie użyć komponent w innej aplikacji, musisz zapewnić mu nową klasę mediator.
Stosuj wzorzec Mediator gdy zauważysz, że tworzysz mnóstwo podklas komponentu tylko aby móc ponownie użyć jakieś zachowanie w innych kontekstach.
Skoro wszystkie relacje pomiędzy komponentami zawierają się w obrębie mediatora, łatwo zdefiniować całkowicie nowe metody współpracy tych komponentów wprowadzając nowe klasy mediatorów, bez konieczności zmian w samych komponentach.
Jak zaimplementować
-
Zidentyfikuj grupę ściśle sprzężonych klas które zyskałyby na niezależności (np. dla łatwiejszego utrzymania lub ponownego użycia).
-
Zadeklaruj interfejs mediatora i określ potrzebny protokół komunikacji pomiędzy mediatorami i innymi komponentami. W większości przypadków, wystarczy pojedyncza metoda otrzymywania powiadomień od komponentów.
Taki interfejs jest kluczowy gdy chcemy ponownie wykorzystać klasy komponentów w innych kontekstach. O ile komponent współpracuje ze swoim mediatorem za pośrednictwem ogólnego interfejsu, można łączyć komponent z innymi implementacjami mediatora.
-
Zaimplementuj konkretną klasę mediator. Najlepiej, gdyby klasa ta przechowywała odniesienia do wszystkich komponentów jakimi zarządza.
-
Można nawet pójść o krok dalej i uczynić mediatora odpowiedzialnym za tworzenie i niszczenie obiektów komponentów. Wówczas mediator zacznie przypominać fabrykę lub fasadę.
-
Komponenty powinny przechowywać odniesienie do obiektu mediatora. Połączenie zwykle nawiązuje się w konstruktorze komponentu, do którego obiekt mediatora przekazywany jest w roli argumentu.
-
Zmień kod komponentów tak, aby wywoływały metodę powiadamiania mediatora zamiast metod powiadamiania innych komponentów. Wyekstrahuj kod zawierający wywołania do innych komponentów do klasy mediatora. Wykonuj ten kod gdy mediator otrzyma powiadomienie od komponentu.
Zalety i wady
- Zasada pojedynczej odpowiedzialności. Możesz wyekstrahować komunikację pomiędzy różnymi komponentami w jedno miejsce, czyniąc ją łatwiejszą do zrozumienia i utrzymania.
- Zasada otwarte/zamknięte. Można wprowadzać kolejnych mediatorów bez konieczności zmiany samych komponentów.
- Można zredukować sprzężenie pomiędzy różnymi komponentami programu.
- Można ułatwić ponowne wykorzystanie komponentów.
- Z czasem mediator może ewoluować do postaci Boskiego Obiektu.
Powiązania z innymi wzorcami
-
Wzorce Łańcuch zobowiązań, Polecenie, Mediator i Obserwator dotyczą różnych sposobów na łączenie nadawców z odbiorcami żądań:
- Łańcuch zobowiązań przekazuje żądanie sekwencyjnie wzdłuż dynamicznego łańcucha potencjalnych odbiorców, aż któryś z nich je obsłuży.
- Polecenie pozwala nawiązywać jednokierunkowe połączenia pomiędzy nadawcami i odbiorcami.
- Mediator eliminuje bezpośrednie połączenia pomiędzy nadawcami a odbiorcami, zmuszając ich do komunikacji za pośrednictwem obiektu mediator.
- Obserwator pozwala odbiorcom dynamicznie zasubskrybować się i zrezygnować z subskrypcji żądań.
-
Fasada i Mediator mają podobne zadania: służą zorganizowaniu współpracy pomiędzy wieloma ściśle sprzęgniętymi klasami.
- Fasada definiuje uproszczony interfejs podsystemu obiektów, ale nie wprowadza nowej funkcjonalności. Podsystem jest nieświadomy istnienia fasady. Obiekty w obrębie podsystemu mogą komunikować się bezpośrednio.
- Mediator centralizuje komunikację pomiędzy komponentami podsystemu. Komponenty wiedzą tylko o obiekcie mediator i nie komunikują się ze sobą bezpośrednio.
-
Różnica pomiędzy Mediatorem a Obserwatorem jest często trudna do uchwycenia. W większości przypadków można implementować je zamiennie, a czasem jednocześnie. Zobaczmy, jak by to wyglądało.
Głównym celem Mediatora jest eliminacja wzajemnych zależności pomiędzy zestawem komponentów systemu. Zamiast tego uzależnia się te komponenty od jednego obiektu-mediatora. Celem Obserwatora jest ustanowienie dynamicznych, jednokierunkowych połączeń między obiektami, których część jest podległa innym.
Istnieje popularna implementacja wzorca Mediator która bazuje na Obserwatorze. Obiekt mediatora pełni w niej rolę publikującego, zaś komponenty są subskrybentami mogącymi “prenumerować” zdarzenia które nadaje mediator. Gdy Mediator jest zaimplementowany w ten sposób, może przypominać Obserwatora.
Jeśli nie jest to zrozumiałe, warto przypomnieć sobie, że można zaimplementować wzorzec Mediator na inne sposoby. Na przykład można na stałe powiązać wszystkie komponenty z tym samym obiektem mediator. Taka implementacja nie będzie przypominać Obserwatora, ale nadal będzie instancją wzorca Mediator.
A teraz wyobraźmy sobie program w którym wszystkie komponenty stały się publikującymi, pozwalając na dynamiczne połączenia pomiędzy sobą. Nie będzie wówczas scentralizowanego obiektu mediatora, a tylko rozproszony zestaw obserwatorów.