poniedziałek, 6 marca 2017

Architektura i CQRS, część 1 - Command

Witam ponownie,
podejmując wyzwanie konkursowe, zapewne każdy stawia sobie za jeden z celów naukę nowych rzeczy - ja również taki cel mam, a jest nim m.in próba zastosowania CQRS w praktyce. O samym Command Query Responsibility Segregation pewnie wielu z was słyszało, a jeśli nie to odsyłam do wujka googla, bo nie chcę tutaj powielać treści, które są już wielokrotnie w internecie opublikowane.
Do tej pory pisząc aplikacje, w tym aplikacje mobilne starałem się trzymać architektury MVC lub dla Xamarina MVVM, która niestety była trudno testowalna i po dłuższym czasie robiło mi się zawsze spaghetti code, którego nikt oprócz mnie by szybko nie zrozumiał. Zarówno w modelach jak i kontrolerach często naruszałem zasadę jednej odpowiedzialności. Po analizie w głowie założeń CQRSa wydaje mi się, że jest receptą na czytelny i łatwo testowalny kod. Kolejną rzeczą, która dla mnie jest nowością (wstyd!) jest kontener DI. Nie twierdzę, że z samego dependency injection  nie korzystałem, ale tak wygodne narzędzie jak kontener było mi obce. No ale od czego jest DSP - kolejna rzecz do toolsetu wpadnie ;) Wybór po krótkich próbach pisania własnego kontenera dla zrozumienia jego działania oraz  krótkiej konsultacji z kolegą Norkiem padł na Autofaca i jak na razie jestem z niego zadowolony.
Ok, przejdźmy zatem do omówienia mojego CQRSa, w jego implementacji wzorowałem się głównie na dwóch źródłach : ebooku od Maćka Aniserowicza i poście Mateusza Stascha, którego prelekcje o CQRSie miałem kiedyś przyjemność posłuchać.

Zaczniemy od ogólnego omówienia. Składowe mojej implementacji cqrsa to:
- Komendy (Command)
- Zapytania (Query)
- Walidatory komend (Validator)
- Zdarzenia (Event)
- Handlery - osobno dla komend i zapytań
- Szyny - szyna komend, szyna zapytań i szyna zdarzeń

To jest core kodu CQRSa, który będę tutaj omawiał. Dodatkowo dochodzą modele danych, widoki i modele widoków, startup aplikacji wraz z rejestrowaniem klas w kontenerze itp, które to rzeczy omówię w kolejnych postach.

Komendy


Zaczniemy od komend, dla nich mam przygotowane dwa interfejsy (marker interface):


public interface ICommand
{
}

public interface ICommandAsync : ICommand
{
}

Jak widać same komendy nic nie robią. Mam dwa rodzaje komend - jedne synchroniczne, drugie asynchroniczne aczkolwiek jak widać, każda komenda asynchroniczna jest również zwykłą komendą. Fakt zastosowania przeze mnie dwóch ścieżek obsługi komend wynika z bogatego wykorzystywania w Xamarinie async i await. Handlery komend wyglądają następująco:

public interface ICommandHandler<in TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

public interface ICommandHandlerAsync<in TCommand> where TCommand : ICommandAsync
{
    Task HandleAsync(TCommand command);
}

Tutaj widać pierwsze złamanie zasady aby komenda nic nie zwracała - w przypadku obsługi asynchronicznej zwracam taska, aby móc wyłapać exceptiony podczas obsługi komendy. Polecam poczytać o stosowaniu async await i dlaczego nie powinno się stosować async void. No i na koniec mamy interfejs szyny komend:

public interface ICommandBus
{
    void SendCommand<TCommand>(TCommand cmd) where TCommand : ICommand;

    Task SendCommandAsync<TCommand>(TCommand cmd) where TCommand : ICommandAsync;
}

Zamieszczę jeszcze implementację szyny komend:


public class CommandBus : ICommandBus
{

 private readonly ILifetimeScope _resolver;
 private readonly IEventPublisher _eventPublisher;

 public CommandBus(ILifetimeScope resolver, IEventPublisher eventPublisher)
 {
  _resolver = resolver;
  _eventPublisher = eventPublisher;
 }

 
 public void SendCommand<TCommand>(TCommand cmd) where TCommand : ICommand
 {
  if (cmd == null)
  {
   throw new ArgumentNullException("command");
  }

  var commandValidator = _resolver.ResolveOptional<IValidator<TCommand>>();
  if (commandValidator != null)
  {
   var res = commandValidator.Validate(cmd);
   if (res.result == false)
   {
    throw new ValidationException(res);
   }
  }

  var commandHandler = _resolver.ResolveOptional<ICommandHandler<TCommand>>();
  if (commandHandler == null)
  {
   throw new Exception(string.Format("No handler found for command '{0}'", cmd.GetType().FullName));
  }

  commandHandler.Handle(cmd);


 }

  
 public async Task SendCommandAsync<TCommand>(TCommand cmd) where TCommand : ICommandAsync
 {

  var commandValidatorAsync = _resolver.ResolveOptional<IValidatorAsync<TCommand>>();
  if (commandValidatorAsync != null)
  {
   var res = await commandValidatorAsync.Validate(cmd);
   if (res.result == false)
   {
    throw new ValidationException(res);
   }
  }

  var commandValidator = _resolver.ResolveOptional<IValidator<TCommand>>();
  if (commandValidator != null)
  {
   var res = commandValidator.Validate(cmd);
   if (res.result == false)
   {
    throw new ValidationException(res);
   }
  }

  var commandHandler = _resolver.ResolveOptional<ICommandHandlerAsync<TCommand>>();
  if (commandHandler == null)
  {
   throw new Exception(string.Format("No handler found for command '{0}'", cmd.GetType().FullName));
  }


  await commandHandler.HandleAsync(cmd);

 }
}

Mimo że implementacja szyny komend jest trochę przydługawa, jej działanie jest bardzo proste i co najważniejsze łatwo rozszerzalne, a na razie: szukamy walidatora komendy, jak jest wykonujemy walidacje, jak walidacja się powiedzie to szukamy handlera komendy, jak go znajdujemy to wykonujemy komende. Implementacje podwaja osobna ścieżka dla asynchronicznych komend i synchronicznych. Kryje się w tym prostym kodzie na prawdę wielka moc - przechodzą przez te dwie metody wszystkie zmiany stanu naszego systemu, chciałem dołożyć walidację i proszę - troszkę kodu i mam machanizm walidacji każdej komendy, będę chciał dołozyć logowanie błędów - proszę bardzo - try catch na handle i dodajemy logowanie. Ponieważ postanowiłem podzielić posta na kilka mniejszych, osobno dla każdego elementu mojego cqrsa to pozwolę sobię jeszcze zamieścić przykładową implementację komendy i jej handlera.


public class AddApiary : ICommandAsync
{
 public Apiary apiary { get; private set; }

 public AddApiary(Apiary apiary)
 {
  this.apiary = apiary;
 }
}

public class AppApiaryHandler : ICommandHandlerAsync<AddApiary>
{
 private SQLiteConnection _conn;

 private IEventPublisher _eventBus;

 public AppApiaryHandler(SQLiteConnection conn, IEventPublisher eventBus)
 {
  _conn = conn;
  _eventBus = eventBus;
 }

 public async Task HandleAsync(AddApiary command)
 {
  _conn.Insert(command.apiary);
  await _eventBus.PublishAsync<Event<Apiary>>(new Event<Apiary>(command.apiary, EventAction.CREATE));
 }
}

Przepraszam za kiepskie wcięcia w kodzie, jak ktoś zna toola, który dobrze koloruje składnie c# i jednocześnie dobrze robi wcięcia to proszę o komentarz. Następny post - Query.

0 komentarze: