Oskar Dudycz

Pragmatycznie o programowaniu

Wzorzec Mediator - czyli jak prosto zwiększyć modularność swojego kodu

2020-08-31 oskar dudyczWzorce Projektowe

cover

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ż).

mediator

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.

  • © Oskar Dudycz 2019-2020