Wiosenna WYPRZEDAŻ

Mediator

Znany też jako: Intermediary, Controller

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

Wzorzec projektowy Mediator

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.

Chaotyczne relacje pomiędzy elementami interfejsu użytkownika

Wraz z ewolucją aplikacji, powiązania między elementami interfejsu użytkownika mogą stać się coraz bardziej chaotyczne.

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.

Współzależności pomiędzy elementami UI

Elementy mogą mieć wiele relacji z innymi. W związku z tym zmiany jednych wpłyną też na inne.

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.

Elementy interfejsu użytkownika powinny komunikować się za pośrednictwem mediatora.

Elementy interfejsu użytkownika powinny komunikować się pośrednio, poprzez obiekt mediator.

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

Wieża kontroli lotów

Piloci statków powietrznych nie rozmawiają ze sobą nawzajem bezpośrednio, gdy ustalają kto następny będzie lądował. Cała komunikacja odbywa się za pośrednictwem wieży kontroli lotów.

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

Struktura wzorca projektowego MediatorStruktura wzorca projektowego Mediator
  1. 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.

  2. 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.

  3. 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.

  4. 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.

Struktura przykładu użycia wzorca Mediator

Struktura klas okna dialogowego.

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.

// Interfejs mediatora deklaruje metodę za pomocą której
// komponenty powiadamiają go o różnych zdarzeniach. Mediator
// może zareagować na te zdarzenia i przekazać wykonanie innym
// komponentom.
interface Mediator is
    method notify(sender: Component, event: string)


// Konkretna klasa mediator. Splątana sieć połączeń pomiędzy
// poszczególnymi komponentami została uporządkowana i
// przeniesiona do mediatora.
class AuthenticationDialog implements Mediator is
    private field title: string
    private field loginOrRegisterChkBx: Checkbox
    private field loginUsername, loginPassword: Textbox
    private field registrationUsername, registrationPassword,
                  registrationEmail: Textbox
    private field okBtn, cancelBtn: Button

    constructor AuthenticationDialog() is
        // Utwórz wszystkie obiekty-komponenty i przekaż bieżący
        // mediator ich konstruktorom by ustanowić połączenia.

    // Gdy coś się zdarzy komponentowi, poinformuje on o tym
    // mediatora. Mediator otrzymawszy powiadomienie może coś
    // zrobić, lub przekazać żądanie innemu komponentowi.
    method notify(sender, event) is
        if (sender == loginOrRegisterChkBx and event == "check")
            if (loginOrRegisterChkBx.checked)
                title = "Log in"
                // 1. Pokaż komponenty formularza logowania.
                // 2. Ukryj komponenty formularza rejestracji.
            else
                title = "Register"
                // 1. Pokaż komponenty formularza rejestracji.
                // 2. Ukryj komponenty formularza logowania.

        if (sender == okBtn && event == "click")
            if (loginOrRegister.checked)
                // Spróbuj znaleźć użytkownika po
                // poświadczeniach.
                if (!found)
                    // Pokaż komunikat błędu nad polem nazwy
                    // użytkownika.
            else
                // 1. Utwórz konto użytkownika korzystając z
                // danych w polach formularza rejestracji.
                // 2. Zaloguj użytkownika.
                // ...

// Komponenty współpracują z mediatorem za pośrednictwem
// interfejsu mediatora. Dzięki temu można używać tych samych
// komponentów w różnych kontekstach poprzez łączenie ich z
// różnymi obiektami mediator.
class Component is
    field dialog: Mediator

    constructor Component(dialog) is
        this.dialog = dialog

    method click() is
        dialog.notify(this, "click")

    method keypress() is
        dialog.notify(this, "keypress")

// Konkretne komponenty nie komunikują się ze sobą. Mają tylko
// jeden kanał komunikacyjny, którym przesyłają powiadomienia
// mediatorowi.
class Button extends Component is
    // ...

class Textbox extends Component is
    // ...

class Checkbox extends Component is
    method check() is
        dialog.notify(this, "check")
    // ...

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ć

  1. Zidentyfikuj grupę ściśle sprzężonych klas które zyskałyby na niezależności (np. dla łatwiejszego utrzymania lub ponownego użycia).

  2. 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.

  3. Zaimplementuj konkretną klasę mediator. Najlepiej, gdyby klasa ta przechowywała odniesienia do wszystkich komponentów jakimi zarządza.

  4. 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ę.

  5. 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.

  6. 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.

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.

Przykłady kodu

Mediator w języku C# Mediator w języku C++ Mediator w języku Go Mediator w języku Java Mediator w języku PHP Mediator w języku Python Mediator w języku Ruby Mediator w języku Rust Mediator w języku Swift Mediator w języku TypeScript