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:
Prześlij komentarz