O CQRS (Command Query Responsibility Segregation) jest w ostatnich latach bardzo głośno. Sam wielokrotnie mówiłem na ten temat prezentację i napisałem artykuł do ProgramistaMag. W tym tekście pominę wstęp teoretyczny i wskoczę prosto w kodzik.
Akcja: BLOGvember! Post nr 8.
W listopadzie w każdy roboczy poranek na devstyle.pl znajdziesz nowy, świeżutki tekst. W sam raz do porannej kawy na dobry początek dnia.
Miłej lektury i do przeczytania jutro! :)
Commands
Podstawowym elementem składowym CQRS są komendy. Zresztą: sama nazwa o tym mówi, prawda? Command pattern, czyli zamknięcie wszystkich informacji potrzebnych do wykonania dowolnej akcji w osobnej, dedykowanej klasie. To tak, jakby… wziąć wszystkie parametry z jakiejś metody i je opakować.
Potrzebujemy w systemie abstrakcji nad pojęciem “komendy”. W .NET doskonale nada się do tego interfejs. Jednak komendy nie potrzebują współdzielić żadnych informacji, nie mają niczego wspólnego. Więc będzie to interfejs pusty, tzw. “marker interface“:
public interface ICommand { }
Tak, wiem, że można do tego zastosować również atrybuty/adnotacje. Owszem, można. Ale atrybuty to po prostu… więcej zachodu. I nie dają możliwości wykorzystania generics, co bardzo nam się przyda.
Każda komenda zaimplementuje taki interfejs. Ale co on daje, skoro jest pusty? Wbrew pozorom, całkiem sporo: daje nam możliwość łatwego odnalezienia wszystkich komend w systemie. Czy to ręcznie, przez programistę (“go to implementation” w R#), czy też przez runtime, za pomocą refleksji. To drugie zastosowanie wyciśniemy już za chwilę.
Skoro mamy komendę, to potrzebujemy “coś, co ją wykona”. Czyli: command handler. No to siup!
public interface IHandleCommand { }
W CQRS każda komenda ma jeden handler. Nie zero, nie wiele. JEDEN. Można o tym myśleć jak o rozbiciu metody na dwie osobne części. Argumenty i ciało metody w dedykowanych klasach.
Ale to jest znowu marker interface: nic nie robi, niczego nie definiuje! To prawda, dlatego potrzebujemy:
public interface IHandleCommand<TCommand> : IHandleCommand where TCommand : ICommand { void Handle(TCommand command); }
Wersja generyczna już przy deklaracji swojego typu mówi, jaką komendę obsługuje. I wreszcie mamy jakąś metodę: Handle().
Brakuje jeszcze jednego elementu spinającego całość: mechanizmu dystrybucji komend w systemie. “Szyny komend”:
public interface ICommandsBus { void Send<TCommand>(TCommand command) where TCommand : ICommand; }
Implementacja tego interfejsu może być różna w zależności od specyfiki systemu. Jedno z rozwiązań to:
public class CommandsBus : ICommandsBus { private readonly Func<Type, IHandleCommand> _handlersFactory; public CommandsBus(Func<Type, IHandleCommand> handlersFactory) { _handlersFactory = handlersFactory; } public void Send<TCommand>(TCommand command) where TCommand : ICommand { var handler = (IHandleCommand<TCommand>)_handlersFactory(typeof(TCommand)); handler.Handle(command); } }
Co my tu mamy? Dependency Injection! Nasza szyna jest zależna od “fabryki handlerów”: funkcji, która na podstawie typu wiadomości potrafi stworzyć instancję jej handlera. To jest abstrakcja doskonała: zostajemy przy czystym .NET (bo Func<>), nie uzależniamy się na tym etapie od żadnego kontenera DI, a jednocześnie dokładnie modelujemy swoje potrzeby. “Dam temu czemuś komendę, a to coś da mi handler”. Wsio.
Events
Koncept CQRS jest rozszerzalny o zdarzenia, czyli Eventy. Zdarzenia mogą posłużyć wielu celom (event sourcing, any1?), ale ich najbardziej podstawowe zastosowanie to: poinformować resztę systemu, że COŚ się wydarzyŁO.
Struktury potrzebne do ich implementacji są lustrzanym odbiciem tego, co znamy już z komend. Czyli:
public interface IEvent { }
public interface IHandleEvent { }
public interface IHandleEvent<TEvent> : IHandleEvent where TEvent : IEvent { void Handle(TEvent @event); }
Różnice zaczynamy zauważać przy “szynie zdarzeń”:
public interface IEventsBus { void Publish<TEvent>(TEvent @event) where TEvent : IEvent; }
Metoda “Send()” z CommandBus została zastąpiona metodą “Publish()”. Koncepcyjnie bowiem zdarzenia mogą mieć dowolną liczbę handlerów. Dowolną, czyli: 0, 1 lub wiele.
Łatwo domyślić się zatem, jak może wyglądać przykładowa implementacja:
public class EventsBus : IEventsBus { private readonly Func<Type, IEnumerable<IHandleEvent>> _handlersFactory; public EventsBus(Func<Type, IEnumerable<IHandleEvent>> handlersFactory) { _handlersFactory = handlersFactory; } public void Publish<TEvent>(TEvent @event) where TEvent : IEvent { var handlers = _handlersFactory(typeof(TEvent)) .Cast<IHandleEvent<TEvent>>(); foreach (var handler in handlers) { handler.Handle(@event); } } }
Jest to PRAWIE to samo co CommandBus, tylko obsługujemy scenariusz “wielu handlerów”. Uzależniamy się od funkcji zwracającej IEnumerable. Pięknie.
No i dobra, ale jak to wszystko… spiąć?
Wire it up!
O Dependency Injection pisałem już wielokrotnie, i pewnie jeszcze nieraz napiszę. I właśnie DI z sensownym kontenerem pomoże nam całość posklejać. Poukładać, dopasować, jak puzzle. I wreszcie: uruchomić.
Poniższy kod przedstawia konfigurację Autofac, mojego ulubionego kontenera DI w .NET. Autofac poważam, ponieważ bardzo mocno separuje on proces konfiguracji i tworzenia kontenera od procesu jego używania. Służą do tego nawet osobne klasy: ContainerBuilder i Container.
Konfiguracja zachowania Autofaca odbywa się w Modułach. Czyli: nie wrzucamy wszystkich porejestrowanych zależności na hurra w jedno miejsce, a sensownie dzielimy pomiędzy dedykowane klasy. Podobny mechanizm znajdziemy zresztą chociażby w StructureMap (Rejestry) czy Castle Windsor (Instalatory).
Zarejestrowanie WSZYSTKIEGO związanego z komendami można umieścić w CommandsModule:
public class CommandsModule : Module { protected override void Load(ContainerBuilder builder) { base.Load(builder); builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => x.IsAssignableTo<IHandleCommand>()) .AsImplementedInterfaces(); builder.Register<Func<Type, IHandleCommand>>(c => { var ctx = c.Resolve<IComponentContext>(); return t => { var handlerType = typeof (IHandleCommand<>).MakeGenericType(t); return (IHandleCommand) ctx.Resolve(handlerType); }; }); builder.RegisterType<CommandsBus>() .AsImplementedInterfaces(); } }
I pozamiatane. Rejestrujemy wszystkie handlery, ich fabrykę, a na koniec samą szynę. Na uwagę zasługuje głównie proces rejestracji fabryki handlerów. Pamiętajcie, że rejestrując własną fabrykę w Autofac nie można używać bezpośrednio przekazanego parametru “c”: trzeba z niego pobrać IComponentContext i dopiero ten kontekst wykorzystać w zwracanej lambdzie. Inaczej: w końcu coś się wywali.
Dzięki tym rejestracjom jesteśmy w stanie dorzucić do kontrolera (czy gdziekolwiek indziej) zależność na ICommandBus i cieszyć się dystrybucją komend w systemie.
Podobnie wygląda sprawa ze zdarzeniami, tylko ze względu na konieczność użycia IEnumerable, rejestracja fabryki handlerów jest nieco bardziej skomplikowana:
public class EventsModule : Module { protected override void Load(ContainerBuilder builder) { base.Load(builder); builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => x.IsAssignableTo<IHandleEvent>()) .AsImplementedInterfaces(); builder.RegisterGeneric(typeof (AllEventsHandler<>)) .As(typeof (IHandleEvent<>)); builder.Register<Func<Type, IEnumerable<IHandleEvent>>>(c => { var ctx = c.Resolve<IComponentContext>(); return t => { var handlerType = typeof(IHandleEvent<>).MakeGenericType(t); var handlersCollectionType = typeof(IEnumerable<>).MakeGenericType(handlerType); return (IEnumerable<IHandleEvent>)ctx.Resolve(handlersCollectionType); }; }); builder.RegisterType<EventsBus>() .AsImplementedInterfaces(); } }
Dwa razy “MakeGenericType()”, jedno po drugim, GENERICS AAALLLEEEEERT!!!!
To tak naprawdę nic szczególnie skomplikowanego. Po prostu potrzebujemy zarejestrować w kontenerze fabryki generyczne bez wykorzystania notacji generycznej.
RUN!
Hyc, koniec.
Zostało dorzucić do swojego systemu, zarejestrować oba moduły w kontenerze i używać. Poimplementować komendy i handlery i cieszyć się, jak się fajnie wywołują. Dorzucić zdarzenia i koncepcyjnie oddzielać AKCJE od REAKCJI.
Have fun.
O jaaa… Takiego posta to się tu nie spodziewałem :D Nieźle wyszło!
GW,
No wiesz, a dlaczego nie? :)
W tym roku jest co prawda przesunięcie w stronę Pani Domu, ale przez lata pisałem technicznie :).
Racja, racja.
Przyzwyczaiłeś, a potem uderzyłeś z zaskoczenia po prostu :)
Co do treści to przykład idealny do wykorzystania jako “starter CQRS”, bez żadnego zbędnego narzutu.
O kurka.. I życie staje się prostsze. Thx :)
Marcin,
Część przyjemności po mojej stronie :)
Dzięki, czytałem kilka artykułów o CQRS, ale dopiero Twój uderza w sedno. Boom! Headshot!
kombain,
To świetnie, tutaj nawet strasznie dużo gadać nie trzeba ani skomplikowanych diagramów rysować.
Kurcze, ale fajnie jest tak zaczynać każdy dzień od Aniserowicza :D
Emi,
No to jest mi bardzo miło, może wyjdę poza listopad z taką praktyką bo feedback jest póki co pozytywny :).
Ooo wracają posty techniczne. Fajno. Plus dla Ciebie.
Piatkosia,
“pojawił się jeden” to jeszcze nie “wracają” :)
Wreszcie doczekalem sie CQRS u Ciebie na blogu. Oby tak dalej :)
Jedno do czego moge sie przyczepic to zgubienie ‘Q’ w w CQRS. ICommand odpowiada za komendy czyli typowe ‘zapisz’ zmiany, jednakze moglbys zaimplementowac tez IQuery, ktore po przekazaniu pewnych parametrow zwrociloby TResult.
Skarbnica wiedzy w tym temacie sa posty Jmmiego Bogard’a (nie wiem jak to powinno sie odmieniac) na blogu LosTechies (http://lostechies.com/). On zaproponowal paczke MediatR, ktora sama w sobie obsluguje te wszystkie elementy i posiada takze wiele rozszerzen.
PS: Sorry za brak polskich znakow :)
mpustelak,
Thx :).
Co do IQuery : to moim zdaniem złe rozwiązanie, zbędna komplikacja, nie ma tego celowo. Zresztą tutaj poruszam temat “write side”, a query to zdecydowanie “read”, nie ma co tego mieszać.
Prawdopodobnie rozwinę temat niedługo.
W pelni sie z Toba zgadzam i czekam na rozwiniecie tematu “read” :)
No! W końcu mięso! A dzień bez `MakeGenericType` to dzień stracony ;-)
Brakuje przy IHandleEvent: where TEvent :IEvent
Dzięki, poprawione
Bardzo przydatny wpis. CQRS to jeden z tych wzorców, który pomaga nie tylko w organizacji kodu i architektury, ale także w organizacji pracy. Jedna osoba może implementować zapis, a druga odczyt. Są to osobne pliki więc praktycznie unikamy merge-conflictów. Stworzyłem kiedyś prosty starter do CQRSa ze skonfigurowanym Simple Injectorem, może komuś się przyda https://github.com/kjendrzyca/SimpleCQRS
Czekam na kolejne wpisy :)
Krzysztof,
Dzięki za link :). Chociaż mi osobiście nie podchodzi, bo: http://devstyle.pl/2016/02/11/antywzrorzec-service-locator/ .
Z pewnością byłoby czyściej zastosować fabrykę handlerów tak jak to pokazałeś i jest to świetny pomysł (będę stosował). W praktyce jednak i tak unikasz wszystkich smrodków związanych z service locatorem, bo jest on używany w 2 klasach w całej aplikacji i służy do tego samego do czego służyłaby fabryka :) A kontener i tak rejestruje się w DI sam, więc to kwestia dyscypliny teamu i CR-ek żeby go nie używali bezpośrednio.
Świetny wpis jak zwykle ;)
Mam jednak jedno pytanie, które nasuwa mi się po przeczytaniu jakiegokolwiek artykułu o CQRS.
Mamy Command, mamy Handler, mamy Event , pierwszy wniosek, który widzę to : teoretycznie asynchroniczne to to jak w mordę strzelił. Rzucam Command na CommandBus, a niech się wykona, a gdzieś tam zostanie podniesiony event jak się wykonało. Logiczne.
A jak to zrealizować w przypadku usług ? WCFa np. ? Przy założeniu, że klient usługi oczekuje na odpowiedź aby pójść dalej ze swoją logiką. Rzucam Command na CommandBus i …?
Rade,
Thx :).
Asynchroniczność jest trudna, skomplikowana i często niepotrzebna. Z nią wiąże się masa problemów (jak transakcje? jak feedback? do tego eventual consistency). Najprostszy przykład to chociażby scenariusz, który opisałeś.
Jeśli idziesz w async, to sposób dystrybucji komend po systemie jest najmniej istotną kwestią akurat.
Właśnie asynchroniczność w moim przypadku jest kompletnie zbędna. Mam usługę, która wykonuje sobie jakieś tam akcje i ma to być synchroniczne.
Teraz chcąc zastosować CQRS muszę jakoś zwracać rezultaty wykonania tych akcji. I tu stwierdzam za każdym podejściem, że tak to chyba się nie da, albo raczej da ale to nie będzie CQRS tylko po prostu request-response.
Co Ty na to ?
Jak na moje oko to to nie jest artykuł dotyczący CQRS tylko takiego poor man’s messaging frameworka (udostepniajacego troszku tego co daje np NServiceBus). Tytuł jest więc mylący. Chyba, że chodziło Ci tylko o Command. Ale duzy plus za kod, bo czasem juz za dużo lałeś wodę w tekstach ;).
Osobiście uważam za fajne podejście ze Query strzela z serwisu aplikacyjnego bezpośrednio do repo readonly, a Command z serwisu aplikacyjnego używa DomainModel (z artefaktami z DDD) i repo z commitem.
SCONRADS,
To nie poezja żeby interpretować na meta-poziomie, czy jest to post o CQRS czy nie :).
To co napisałem działa in-process. NServiceBus działa pomiędzy procesami/maszynami i może być kolejnym poziomem ewolucji mojego rozwiązania.
Query/Command i inny sposób dostępu do danych: jak najbardziej, to jest “core” CQRSa. Chociaż, jak pisałem w którymś komentarzu wyżej, fanem “obiektu query” nie jestem. Repozytorium zresztą też nie.
Super post! dzięki wielkie za niego :) piątka
UKS,
PIątka zatem :) thx
A ja chciałbym żebyś przy którymś z kolejnych wpisów rozwinął jak dobre korzystać z Modułów w Contenerze DI :)
Chodzi mi o sytuację czy masz moduł per projekt, i czy zdarza Ci się reużywać tych modułów. W kilku miejscach można przeczytać że cała konfiguracja DI powinna być zawsze tylko w jednym miejscu (najczęściej projekt WebApp czy podobny). Jak więc ma się to do modułu per projekt i składania takiej konfiguracji w WebApp z modułów.
Krzysiek,
Chodzi o “composition root”, czyli miejsce w APLIKACJI zawierającej konfigurację kontenera. Takie miejsce powinno być jedno. Ale ta konfiguracja może używać modułów. Więcej można poczytać tutaj: http://devstyle.pl/2016/01/11/di-3-calls-pattern/ .
Przy okazji tego wpisu postanowiłem przyjrzeć się bliżej CQRS. Obejrzałem prezentację, przeczytałem artykuł, obejrzałem jeszcze inną prezentację. I mam problem. Lubię kiedy nazywa się rzeczy po imieniu, a nie mruga okiem i umawia, że będzie się je traktować inaczej (“choćbyście nie wiem jak się starali, to nie zrobicie z Tico Ferrari” – jak śpiewał pewien kabaret). I tu w CQRS mówi się o poleceniu, a owo polecenie jest de facto zestawem danych (nic nie robi), właściwym poleceniem okazuje się ów handler (zastanawiam się jak to przetłumaczyć, bo dobrze byłoby znaleźć bardziej adekwatne pojęcie niż “uchwyt”). Dlaczego zatem nie nazywać owych bytów dokładnie tak jak wskazuje ich przeznaczenie? Ów handler niech będzie poleceniem, a owo “polecenie” jest danymi.
Paradoksalnie to pytanie (IQuery) jest bliższe poleceniu (bo się wykonuje) niż owo ICommand.
Przypomnę tylko, że Meyer jako polecenia rozumiał procedury (czyli metody, które coś robią i nie zwracają wyniku), a za zapytania funkcje (które też coś robią, ale ma to na celu wyłącznie uzyskanie zwracanego wyniku).
Paskol,
To o czym piszesz to szczegóły implementacyjne. CQRS to rozdzielenie read od write, czyli słusznie Meyera przywołujesz. Wkrótce napiszę osobny post na ten temat.
Ale właśnie o szczegóły chodzi. Bo w nich kryje się diabeł (DevDevil ;) ). No i niestety owe szczegóły stały się de facto wzorem (nie wzorcem, ale wzorem), szablonem, który raczej bezrefleksyjnie się powiela (pachnie kopypasteryzmem).
Oczywiście poczekam na wpis na blogu.
P.S. We wspomnianej prezentacji bardzo podobał mi się koncept odejścia od ORM-a. Wydawało mi się, że ja z racji wieku jestem w kwestii ORM malkontentem (choć mam konkretne zarzuty, ale ORM to podobno takie nowoczesne) no i tutaj widzę, że kolejne pokolenia dojrzewają do uświadomienia sobie przerostu formy nad treścią dla ORM. Twój patent z widokiem SQL ja zrealizowałem już w 2009 (wtedy jeszcze w innym języku) za pomocą elastycznego systemu procedur składowanych (zrobiłem z tego podyplomówkę). Obecnie w C# używam go we współpracy z Dapperem. To podejście bardzo dobrze oddaję ideę podziału odpowiedzialności. A odpowiedzialność za utrwalanie danych oraz ich pozyskiwanie spoczywa na bazie danych.
Bardzo prosty i czytelny przykład.
Nie tak dawno temu przygotowałem sobie podobnego gotowca z uwzględnieniem query (https://github.com/devamator/CQRS). Moje ego skoczyło co najmniej o dwa poziomy, bo z tego co widzę jest dość podobnie.
Po przeczytaniu twojego posta uzupełniłem swoje repo o konfigurację autofaca, bo wcześniej robiłem to trochę bardziej naokoło, a dodatkowo będę miał na przyszłość żeby nie klepać jej za każdym razem od nowa :)
[…] CQRS+DI: implementacja w C# i Autofac […]
[…] CQRS+DI: implementacja w C# i Autofac […]