Przez długi czas wzorzec projektowy Mediator kojarzył mi się z bardzo skomplikowanym mechanizmem, który jest dedykowany dużym systemom i którego poznania trochę unikałem. Okazuje się, że strach ma wielkie oczy, a Mediatora można sprowadzić do dość prostego komponentu, który z powodzeniem można stosować nawet w małych aplikacjach.
Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce, to koniecznie sprawdź mój wpis o wzorcach projektowych.
Założenia wzorca projektowego Mediator
Mediator jest jednym z behawioralnych wzorców projektowych. Chcąc zrozumieć założenia i działanie wzorca Mediator można przedstawić na przykładzie instytucji mediatora w procesie mediacji sądowych. Zadaniem mediatora w sądzie jest pośredniczenie w komunikacji między dwoma lub więcej podmiotami. Identycznie sytuacja wygląda w przypadku wzorca projektowego. Zadaniem mediatora jest pośredniczenie w komunikacji między obiektami w systemie. Jednocześnie, wykorzystanie mediatora ogranicza bezpośrednią komunikację między obiektami i zmusza je do komunikacji za pośrednictwem dedykowanego komponentu.
Zwykle, klasa będąca mediatorem wystawia metodę, która służy do komunikacji między obiektami. Zadaniem mediatora jest odebranie wiadomości od jednego obiektu i przekazanie jej do innego. Na diagramie poniżej znajdziesz przykład aplikacji, w której wykorzystany został wzorzec mediator do komunikacji w grupie znajomych.
W klasie Mediator
zaimplementowano metodę send
, która przyjmuje wiadomość od użytkownika, a następnie przekazuje ją do drugiego. Każdy użytkownik dysponuje metodą receive
do otrzymywania wiadomości.
Spotkałem się również z podejściem z dwiema metodami. Mogą to być np. metody command
i query
. Głównym założeniem było wystawienie dedykowanych metod dla wysyłania zapytań i poleceń w systemie. Celem poleceń było wykonywanie akcji w systemie. Z kolei celem zapytań było zwracanie wyników. Takie podejście do tematu może stanowić dobry wstęp do implementacji wzorca CQRS w budowanym systemie. Oczywiście nic nie stoi na przeszkodzie, by zarówno zapytania, jak i polecenia były obsługiwane przez mediatora za pomocą jednej metody.
Mediator – wady i zalety
Wykorzystanie Mediatora niesie ze sobą kilka korzyści. Po pierwsze, osobny komponent dedykowany komunikacji pomaga pisać kod zgodny z zasadą Single Responsibility Principle. Drugą istotną zaletą jest redukcja couplingu między klasami. Obiekty, które się komunikują ze sobą, nie muszą o sobie nic wiedzieć. Mała liczba zależności pozwala również w łatwy sposób rozszerzać system o nowe obiekty. Co więcej, testowanie takiego systemu powoduje mniej komplikacji. W końcu scentralizowany komponent odpowiedzialny za komunikację sprawia, że proces debuggowania jest potencjalnie prostszy. Z góry znana jest ścieżka wywołań metod i przepływu danych w aplikacji, co może przyspieszyć detekcję wadliwego fragmentu kodu.
Scentralizowany komponent jest jednak potencjalnym zagrożeniem. Mediator stanowi potencjalny Single Point of Failure, a błąd w nim może wpłynąć na całą aplikację. Mimo że coupling między obiektami zostaje zmniejszony, to powstaje coupling między obiektami a Mediatorem. Mediator staje się głównym obiektem w systemie i wiele innych obiektów od niego zależy, przez co wdrażanie zmian w samym obiekcie Mediatora może być problematyczne.
W ostatecznym rozrachunku uważam, że jeśli w aplikacji wykorzystanie Mediatora ma sens, to gra jest warta świeczki. Moim zdaniem, przedstawione problemy są do zaakceptowania, patrząc na zysk, jaki można uzyskać, wykorzystując ten wzorzec projektowy.
Wzorzec projektowy Mediator w praktyce
Do przedstawienia działania Mediatora w kodzie zaimplementuję komponenty obecne na przedstawionym wcześniej diagramie. Do implementacji wykorzystałem TypeScript, ale dołożyłem wszelkich starań, by przykłady były zrozumiałe i możliwe do implementacji w większości popularnych języków programowania.
class Message {
public readonly text: string;
public readonly recepient: string;
public constructor( { text, recepient }: { recepient: string; text: string; } ) {
this.recepient = recepient;
this.text = text;
}
}
class Person {
public constructor( public readonly name: string ) {}
public receive( message: Message ) {
console.log( 'Thanks!', message );
}
}
class Mediator {
public constructor( private readonly _people: Person[] = [] ) {}
public send( message: Message ) {
const recepient = this._people.find(
person => message.recepient === person.name
);
if ( !recepient ) {
throw new Error( 'Recepient not found!' );
}
recepient.receive( message );
}
}
const john = new Person( 'John' );
const bob = new Person( 'Bob' );
const tom = new Person( 'Tom' );
const mediator = new Mediator( [ john, bob, tom ] );
mediator.send( new Message( { text: 'Hello!', recepient: 'Bob' } ) );
mediator.send( new Message( { text: 'Have a good day!', recepient: 'Tom' } ) );
Rezultatem uruchomienia powyższego kodu będzie wylogowanie dwóch logów w konsoli z treścią wysłanych wiadomości. Tak zaprojektowany mediator pozwala na komunikację zewnętrznych obiektów z użytkownikami za pośrednictwem mediatora. Nieco przypomina to wykorzystanie wzorca Proxy. Taka konstrukcja miałaby sens w systemie wysyłającym np. notyfikacje poszczególnym osobom.
W przedstawionym przykładzie więcej sensu miałaby implementacja mechanizmu umożliwiającego komunikację między użytkownikami. Aby ją zaimplementować, wystarczy nieco zmodyfikować wcześniej przedstawiony przykład.
Implementacja komunikacji dwustronnej między osobami w kodzie wygląda następująco.
class Message {
public readonly text: string;
public readonly recepient: string;
public constructor( { text, recepient }: { recepient: string; text: string; } ) {
this.recepient = recepient;
this.text = text;
}
}
class Person {
public constructor(
private readonly _mediator: Mediator,
public readonly name: string
) {}
public receive( message: Message ) {
console.log( 'Thanks!', message );
}
public send( message: Message ) {
this._mediator.send( message );
}
}
class Mediator {
private readonly _people: Person[] = []
public constructor() {}
public send( message: Message ) {
const recepient = this._people.find(
person => message.recepient === person.name
);
if ( !recepient ) {
throw new Error( 'Recepient not found!' );
}
recepient.receive( message );
}
public addPerson( person: Person ) {
this._people.push( person );
}
}
const mediator = new Mediator();
const john = new Person( mediator, 'John' );
const bob = new Person( mediator, 'Bob' );
const tom = new Person( mediator, 'Tom' );
mediator.addPerson( john );
mediator.addPerson( bob );
mediator.addPerson( tom );
john.send( new Message( { text: 'Hello!', recepient: 'Bob' } ) );
bob.send( new Message( { text: 'Hi, have a good day!', recepient: 'John' } ) );
Podsumowanie
Jeśli w Twoim systemie istnieje pajęczyna zależności między obiektami, to Mediator może być idealnym kandydatem do rozwiązania tego problemu przy najbliższym refaktorze. Daj znać w komentarzu, czy miałeś/aś okazję korzystać już z tego wzorca projektowego oraz jakie korzyści Ci dał. Zachęcam do sprawdzenia artykułów podlinkowanych poniżej. Wiedza w nich zawarta istotnie pozwoliła mi zrozumieć założenia Mediatora i zastosować go w praktyce.
Książkę Design Patterns: Elements of Reusable Object-Oriented Software możesz kupić klikając w TEN link. Jest to link referencyjny, dzięku któremu zarobię na rozwój tego bloga.
Źródła i materiały dodatkowe
- Design Patterns: Elements of Reusable Object-Oriented Software – Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994)
- Mediator Design Pattern
- Mediator – Refactoring Guru
- MediatR : CQRS i wzorzec projektowy Mediator w ASP.NET CORE
- The University of North Carolina – Mediator
- Mediator Vs Observer Object-Oriented Design Patterns
- GoFPatterns – Mediator
- DigitalOcean – Mediator Design Pattern in Java
- Mediator/Middleware Pattern
Zapisz się na mailing i odbierz e-booka
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.