Wzorzec Mediator - czyli jak prosto zwiększyć modularność swojego kodu
Cześć!
Lato się kończy! I to deszczem, w każdym razie tutaj gdzie siedzę. Ostatnie maile były dosyć letnie i to na dwójnasób. Letnie w sensie wakacyjne jeśli chodzi o tematykę i letnie - w sensie niezbyt gorące jeśli godzi o wiedzę techniczną. Dzisiaj postaram się nieco bardziej “mięsnie”.
Chciałbym Ci dzisiaj opowiedzieć o jednym z prostszych wzorców, nieco mniej popularnych, ale bardzo prakrycznych. Wzorzec mediator.
Każdy z nas myślę miał sytuację w dzieciństwie gdzie mówił “Ja się do Jaśka nigdy już nie odezwę!”, a następnie mówił Krzyśkowi - “Krzysiek, powiedz Jaśkowi, że to i to”. “Za moich czasów” (przed RODO) w szkole na w alentynki można było wrzucić do skrzynki liścik, który potem rozniesiony przez dyżurnych do adresatek. Które zwykle nie odpisywały. W sumie nie dziwne, jak się wysyłało te listy dla jaj w imieniu kolegów… No cóż głupota młodości.
Zarówno Ty jak i ja pewnie masz w swoim projekcie osobę typu “przynieś, podaj pozamiataj”. Taka osoba zwie się różnie. Czasem SCRUM master, czasem Product Owner. W zależności od lokalnej konwencji. Ideą tej osoby jest to, żebyśmy my mogli się skupić na pisaniu kodu (w końcu nie po to jesteśmy programistami, żeby rozmawiać z ludźmi), a on(a) przekazuje nasze pytania do “biznesu”. No i czasem przynosi nam odpowiedzi. Często nie. Tak jak poczta walentynkowa.
Taką właśnie ideę ma wzorzec mediator. Jego ideą jest to, żeby każdy mógł się skupić na swoim zadaniu. Pomaga to w lepszym rozdzieleniu odpowiedzialności i zachowaniu Single Responsiblity Principle. Diagramik (no bo musi być przecież).
Czyli tak jak mówiłem - “Przekaż Jaśkowi, że jest mi przykro”.
No dobrze, ale gdzie praktyczne zastosowania? Ja uwielbiam używać implementacje tego wzorca - bibliotekę MediatR.
public interface IMediator
{
Task<TResponse> Send<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default);
Task<object?> Send(object request,
CancellationToken cancellationToken = default);
Task Publish(object notification,
CancellationToken cancellationToken = default);
Task Publish<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : INotification;
}
Jest to interfejs genialny w swej prostocie. Ma w zasadzie dwie metody (bo drugie przeładowanie to po prostu typ generyczny):
- Send - wyślij do konkretnego odbiorcy wiadomość (i dostań ewentualnie odpowiedź),
- Publish - wyślij wiadomośc do wielu odbiorców (nie oczekujac odpowiedzi).
Możemy bardzo prosto poprzez dodanie prostych wrapperów użyć tej biblioteki jako:
- event busa - do publikowania zdarzeń. Zwykle jak to robimy to nie wiemy ilu jest odbiorców i kto obsłuży te zdarzenie (coś jak wysyłanie newsletterów, niby wiesz ile osób jest na liście, ale kto odczyta - zagadka)
public class EventBus: IEventBus
{
private readonly IMediator _mediator;
public EventBus(IMediator mediator)
{
_mediator = mediator;
}
public async Task Publish<TEvent>(params TEvent[] events)
where TEvent : IEvent
{
foreach (var @event in events)
{
await _mediator.Publish(@event);
}
}
}
- command busa - do wysyłania komend. Tutaj po prostu wysyłamy komendę z poleceniem wykonania konkretnej operacji. Kto ją wykona jest nam obojętnie. Byleby było zrobione. Nie oczekujemy odpowiedzi.
public class CommandBus: ICommandBus
{
private readonly IMediator _mediator;
public CommandBus (IMediator mediator)
{
_mediator = mediator;
}
public Task Send<TCommand>(TCommand command)
where TCommand: ICommand
{
return _mediator.Send(command);
}
}
- query busa - do wysyłania query. Tutaj po prostu wysyłamy query z żądaniem zwrócenia wyniku. Też nas nie interesuje kto tego dokona, ważne żeby zwrócił poprawną odpowiedź.
public class QueryBus: IQueryBus
{
private readonly IMediator _mediator;
public QueryBus (IMediator mediator)
{
_mediator = mediator;
}
public Task<TResponse> Send<TQuery, TResponse>(TQuery query)
where TQuery : IQuery<TResponse>
{
return _mediator.Send(query);
}
}
Takie podejście pozwala w prosty sposób i bez wielkiego narzutu zaimplementować CQRS i Event-Driven Design. Pozwala też łatwiej podzielić kod na mniejsze fragmenty/komponenty. Taki kod jest też łatwiej testowalny.
Zachęcam do zabawy moim framework GoldenEye - tam są te klasy już zaimplementowane wraz z boilerplate itd. Łatwo pozwoli to myślę pobawić się tym wzorcem i sprawdzić czy u Ciebie się też przyda. Więcej tutaj: https://github.com/oskardudycz/GoldenEye/tree/master/src/Core/Backend.Core.DDD.
Sam MediatR zaprezenowałem w akcji tutaj: https://github.com/oskardudycz/EventSourcing.NetCore#2-message-bus-for-processing-commands-queries-events---mediatr, plus wiele przykładów w Samplach - https://github.com/oskardudycz/EventSourcing.NetCore/tree/master/Sample.
Daj znać jeśli masz jakieś pytania. Śmiało komentuj czy co o tym sądzisz i pisz jeśli masz jakieś uwagi.
Pozdrawiam serdecznie!
p.s. w ramach treningu wzorca MediatR zapytaj się swojej koleżanki i kolegi czy nie chcą poczytać newslettera o programowaniu, jeśli chcą to wiesz kto się na pewno ucieszy z polecenia.