CQRS (Command Query Responsibility Segregation) ma wiele odmian. Jeden napisze tak, drugi napisze inaczej. I FIGHT – święta wojna gotowa. O podstawach CQRS, z odrobiną historii i przykładami, wkrótce jeszcze napiszę. Dziś: o esencji, o “corze”, o serduszku <3.
Akcja: BLOGvember! Post nr 18.
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! :)
O co spór?
Różnice w implementacji podejścia CQRS zaobserwować można na każdym kroku. Każdy programista ma swoje racje i nie ma jednej poprawnej drogi. Jak to zwykle bywa: “whatever works“. Z dopiskiem: “in a given context“.
Niedawno zaprezentowałem na blogu swoje podejście do składowych komponentów CQRS i ich rejestracji w kontenerze DI. Niedługo pojawi się kolejny, dłuższy, tekst na podobny temat. Można tam poczytać o takim tworze jak ICommandBus – komponencie, którego rolą jest dystrybucja wiadomości w danym systemie. Dla mnie to piękna koncepcja, niosąca bardzo wiele korzyści i praktycznie pozbawiona wad. Znajdą się jednak zwolennicy teorii, że czegoś takiego jak “szyna komend” w ogóle nie powinno się implementować. I git, nie ma się o co bić.
W moich projektach, na moje potrzeby, w MOIM KONTEKŚCIE, zrównoleglanie operacji najczęściej nie było potrzebne. Wyznaję zasadę, aby wszystko było tak proste jak to tylko możliwe. Jeśli da się więc osiągnąć założenia biznesowe bez wielowątkowości, komunikacji między procesami czy asynchroniczności, to tak właśnie zrobię. Można jednak natknąć się stwierdzenia, że “implementowanie CQRS na jednym wątku jest bez sensu“. Hmm… U mnie tak to właśnie działało: komenda -> handler -> event -> handler -> event -> handler – wszystko na jednym wątku, bez zrównoleglania. I sprawdziło się wyśmienicie.
Wreszcie: “jakie dane powinny zwracać komendy?“. W MOIM podejściu: żadnych (o tym też się jeszcze rozpiszę). Komenda to VOID, i już. Ileż razy byłem jednak przekonywany, że to bardzo ograniczające i nieprawidłowe podejście…
Wiesz co? Wszystkie powyższe spory to tylko szczegóły implementacyjne.
Esencja CQRS
Samo serce CQRS to jedno proste przykazanie: oddziel zapis od odczytu. Tylko tyle i aż tyle.
CQRS = oddzielenie zapisu od odczytu
Jeśli masz problemy opisane powyżej, to jest już dobrze. To są problemy, które fajnie jest mieć. Przynajmniej w porównaniu z alternatywą.
W CQRS bierzemy to:
Czyli JEDEN MODEL, służący do wszystkiego. I do zapytań, i do wykonywania logiki, i do reprezentacji danych. Duży zestaw skomplikowanych klas. Zestaw, który się nie sprawdza.
Zamieniamy to na:
Uświadamiamy sobie, że potrzebujemy więcej niż jeden model. Potrzebujemy przynajmniej dwa modele: jeden przystosowany do odczytu danych i jeden dedykowany do przetwarzania/zapisu.
Jak je zaimplementujemy? To kwestia drugorzędna. Ja mam swój sposób, Ty możesz mieć inny sposób. Po takiej operacji i tak jesteśmy w o wiele lepszej pozycji, niż jeszcze niedawno.
Bo:
Asking a question should not change the answer
Bertrand Meyer
BUM. Koniec.
Bonus 1
O CQRS możesz posłuchać w moim podkaście nagranym z jednym z dwóch “ojców” tego podejścia: DevTalk#14 – CQRS with Udi Dahan. Bardzo ciekawa audycja w kontekście mniej technicznym, takie treści trudno znaleźć więc bardzo polecam.
Bonus 2
Dlaczego ten obrazek pasuje do dywagacji o CQRS? :)
Policjantom się nie spodobało, bo to wyjaśniłeś za szybko! W CQRS ma być powoli, trudno i skomplikowanie! Panów policjantów nie interesuje czy dowieziesz paczke do klienta, panów policjantów interesuje czy twoja komenda zwraca void.
A tyś Pan to na dziecięcej zabawce rozrysował:)
Maciek,
Haha no tak, bardzo fantazyjna interpretacja :).
Policjanci zawsze chodzą parami, bo jeden umie czytać a drugi pisać ;)
A to też, też:)
Tajest :)
Bo jeden czyta, a drugi pisze :P
Tajest :)
Ja bym jeszcze dodał w ramach tej esencji, że implikacją takiego podziału jest możliwość skalowania asymetrycznego aplikacji oraz doboru technologii/struktur danych pod operacje zapisu i odczytu. Zgadzam się też, że trwa od dawna wojna o “poprawną” implementację tego wzorca. “ooooo aaaa CommandBus to antywzorzec!”, “lol, CommandHandler zwraca VOID? To jak pobiore ID nowo utworzonego elementu? to bez sensu i tylko komplikujesz sobie życie”. Najlepsze jest, że potem wiele osób w pewnym momencie nieświadomie zaczyna implementować CQRS wydzielają komponenty pod zapis i odczyt, ponieważ jest to wygodne i zazwyczaj nie ma problemu ze znalezieniem tej granicy pomiędzy read i write.
Anyway, dobry tekst i czekam na kolejną część :)
Darek,
Skalowanie (zarówno aplikacji jak i zespołu) to osobna historia, wynikająca bezpośrednio z tego podziału. I oczywiście, jest jedną z ogromnych zalet :).
A do ID odniosę się w osobnym poście już wkrótce.
W temacie ID można by się pewnie sporo rozpisywać, ale dla mnie najważniejszym aspektem jest, że ID to dana czysto techniczna.
Warstwa dostępu do danych jak najbardziej z niej korzysta i tam IDiki latają tam i z powrotem.
Według mnie CQRS w swoim założeniu lata pomiędzy warstwą interfejsu (UI,WCF,Bus,itp…), a na tej warstwie powinny latać identyfikatory bardziej biznesowe (np.: numer faktury a nie jej id).
A skoro do Command trafił na wejściu identyfikator biznesowy, to po co go przesyłać dalej, skoro w kontekście wywołania swojej komendy już go masz.
Devamator,
To co opisujesz to Natural Key (https://en.wikipedia.org/wiki/Natural_key), ale nie każdy obiekt taki posiada. Zresztą po tych komentarzach napisałem dziś osobny post na ten temat, puszczę w przyszłym tygodniu :).
Było by łatwiej gdyby nie to że ktoś kiedyś wymyślił formularze html-owe które standardowo robią post-a na ten sam adres z którego przyszły – czyli wyświetlasz stronę z formularzem no więc ta strona powinna też obsługiwać zapis danych z tego formularza. No i jakoś mnóstwo ludzi się do tego przywiązało bo wygodne i intuicyjne. No i jak tu przylepić do tego CQRS bez mieszania wszystkiego ze wszystkim?
No, ale GET i POST to na serwerze dwie oddzielne akcje kontrolera. CQRS w zasadzie nie dotyczy warstwy HTTP, tylko wszystkiego co poniżej. W związku z tym Twój GET w kontrolerze będzie wołał read model z części aplikacji do odczytu, natomiast POST będzie przekazywał command dalej do ComamndBusa czy bezpośrednio do CommandHandlera. Nie ma tu żadnego problemu.
Tyle że w tak oczywistym miejscu musisz CQRSa wepchnąć gdzieś w niższe warstwy, bo Twój MVC kontroler obsługuje zarówno odczyt jak i zapis w GUI (a często jeden POST jednocześnie zapisuje dane i generuje widok w HTMLu). Czyli jak na moje oko oczywista skucha tego obiecującego modelu – zamiast rozwiązać problem omijamy go przez wrapowanie, abstrakcję i schowanie gdzieś głębiej.
RG,
Jeśli kontroler obsługuje jednocześnie odczyt i zapis i jeszcze do tego generuje widok to coś jest nie tak z kontrolerem. CQRS nie ma tu nic do rzeczy, bo niezależnie od niego jest to programowanie z proszeniem się o kłopoty.
“Whatever works” – każdy stosuje, w końcu chodzi o to żeby było łatwo i prosto, a jak się przy okazji da dobrze to super.
I raczej wszystko jest OK z kontrolerem bo to standardowa praktyka żeby w jednym requeście POST zapisać dane a potem wygenerować view. Bo po co robić dwa requesty do serwera jeśli można jeden?. Ale to też oznacza że zapis i odczyt następują w jednej transakcji więc separacja jednego od drugiego jest tylko logiczna – można sobie oddzielić kod z tych obszarów i MVC to robi. Nadal jednak view musi czekać aż dane zostaną zapisane.
tak więc czuję się nie do końca usatysfakcjonowany odpowiedzią że standardowa obsługa formularzy/POSTów jest niedobra ale jak chcesz mieć dobrą w CQRS to musisz sobie jakoś inaczej poradzić. No właśnie, jak dokładnie, i jak zrobić żeby nie było to trudniejsze niż nie stosowanie CQRSa?
RG,
To, że praktyka jest standardowa, nie oznacza, że jest dobra. Rzekłbym, że w tym przypadku jest wręcz przeciwnie. Odeślę do PRG (Post/Redirect/Get): https://en.wikipedia.org/wiki/Post/Redirect/Get .
I znowu: to nie ma związku z CQRS, niezależnie od jego stosowania bądź nie, POST zwracający widok to podejście co najmniej… dyskusyjne.
czyżby początki świętej wojny? :) Myślę że obsługa transakcji to ważny temat i nie da się go obejść ani zignorować – w końcu wyjdzie i okaże się że ta separacja odczytu od zapisu to coś trudniejszego niż tylko organizacja kodu, a jeśli oddzielisz od siebie te operacje do niezależnych transakcji to napotkasz problemy ze współbieżnością / synchronizacją operacji.
Co do Post/Redirect/Get – to już ma znamiona dogmatu, w końcu niezależnie od typu requestu wysyłasz dokładnie ten sam pakiet danych. To nie dotyka istoty problemu i samo majstrowanie przy urlach go nie usuwa.
PS nie żebym się czepiał, mam jakieś swoje metody ‘CQRS-like’ które dobrze sobie radzą z posprzątaniem bajzlu wprowadzanego przez ‘modele’ w MVC, ale byłem ciekaw jakichś głębszych wniosków.
RG,
Formularze domyślnie tak robię, ale dodajesz jeden atrybut na FORM i zachowanie się zmienia. Nie ma problemu absolutnie żadnego. I, jak pisze Darek – GET i POST pod ten sam URL to dwie różne akcje, mogą być nawet w różnych kontrolerach.
Widzę, że pożyczyłeś profesjonalny tablet do rysowania diagramów :)
A co do wpisu, to CQRS jest jednym z wielu możliwych podejść zapewniających SOLID na poziomie architektury projektu.
Devamator,
Jeśli jakiś diagram nie mieści się na tym tablecie to znaczy że jest zbyt skomplikowany :).
A czy CQRS zapewnia SOLID… tego bym nie powiedział. Z CQRS też się da zrobić spaghetti. Tyle że to spaghetti będzie osobno czytało, a osobno zapisywało :).
Maciek,
Masz rację, żadne rozwiązanie nie gwarantuje że zostanie zachowany solid. To czy kod będzie spełniał te wymogi czy nie to już tylko dobra wola programisty :)
Jak widać mój myślowy skrót nie jest tak jednoznaczny jak mi się wydawało w trakcie pisania.
Jednak według mnie, tak jak interfejsy ułatwiają testowanie, tak cqrs ułatwia trzymanie się tych zasad, a przynajmniej zauważyłem, że łatwiej programistom je spełnić.
Zespół z którym pracuję jakiś czas temu stwierdził (i to niemal jednogłośnie), że to podejście ułatwia zrozumienie całości (dziel i rządź), testy biznesowe oraz dalsze utrzymanie projektu (poprawki, zmiany) nawet w obszarach, których się wcześniej nie dotykało. A zmiany powodują wymiernie miej dziwnych zachować w innych częściach systemu.
Poczekam aż dojdziesz (he, he :) ) do kwestii komend i ich uchwytu. Być może wdam się w polemikę.
Co do Bonusu numer 2 -> Obrazek pasuje bo nie pisze się wpisów na bloga o CQRS prowadząc samochód? ;)
MACIEJBURCHARDT,
A to też :). To już trzecia różna odpowiedź!
Kiedyś zrobię sobie tatuaż. Na jednym przedramieniu będzie “whatever works”, a na drugim “in a given context”. Dzięki za inspirację!