CQRS == Enterprise?

Ten wpis pierwotnie miał traktować o zasadności wprowadzenia CQRS do aplikacji opertej o mikroserwisy. Jednak przed rozpoczęciem właściwej częsci artykułu chciałem, abyśmy mieli spójną definicję tego konceptu. Trochę się rozpisałem… i uznałem, że warto wynieść ten tekst do osobnej publikacji, którą łatwo będzie zalinkować w razie potrzeby. Dlaczego?  W moim odczuciu wielu programistów mylnie utożsamia ten wzorzec (sic!) z jakimś wielkim molochem klasy Enteprise, wymagającym zaawansowanej infrastruktury, zaplecza specjalistów do utrzymania i milionem linii kodu potrzebnych do poprawnej implementacji. W konsekwencji wiele osób najzwyczajniej w świecie boi się wprowadzić go w swoich projektach. Myślisz, że przesadzam? Nie muszę szukać daleko, aby pokazać Ci o czym mówię. W zeszłym roku opublikowałem tweeta z pytaniem, czy ktoś byłby zainteresowany darmowym kursem na YT właśnie o mikroserwisach. Pierwsza odpowiedź?

 

 

Nie wiem z czego wynika taka obawa przed CQRS. Być może jest to pochodna dość mocnego spopularyzowania się tego konceptu przez ostatnie kilka lat i efektu podobnego do zabawy w „głuchy telefon”. Ktoś kiedyś usłyszał o CQRS, ale było tam też coś o jakimś Event Sourcingu. Zdarzenia? Struktury danych niezoptymalizowane pod odczyt? Dwie bazy danych?! Service Bus?! O nie nie. Ten CQRS to musi być jakiś twór dla korpoludków zmagających się w bankowości i ubezpieczeniach. Tak jak wspomniałem wcześniej, nastąpiło swoiste pomieszanie i przeniknięcie definicji, dwóch konceptów, które adresują inne problemy, a dodatkowo nie zawsze muszą współistnieć. Zacznijmy więc definiowanie pojęć niejako od drugiej strony.

 

Event Sourcing

Pracując z bazą danych czy to na studiach, czy to później w pracy, przyzwyczailiśmy się do modelowania tabel SQL czy innych struktur, w taki sposób, aby przechowywały one aktualny stan naszej aplikacji. Weźmy na przykład bardzo uproszczony model książki:

 

 

To co widzisz powyżej to umowny wiersz w jakiejś bazie SQL, który zawiera informacje o książce pt. „C# in Depth”. Są tu informacje, które większość z nas umieściłaby w takiej formie, gdyby przyszło nam dokonać implementacji księgarni online. Mamy Id, tytuł, cenę którą wyświetlimy na stronie oraz stan magazynowy, na podstawie którego dokonamy walidacji. BINGO! I faktycznie w przypadku, gdy nasza aplikacja jest bardzo prostym CRUD-em bez głębszej logiki biznesowej to wszystko jest OK. Problem pojawia się jednak wtedy, gdy musimy odpowiedzieć na pytania nieco bardziej skomplikowane niż to ile książek mamy na stanie? Przykładowo:

  • Ile dostaw zawierało książkę „C# in depth”?
  • Ile razy książka została przeceniona?
  • Ile razy ktoś dokonał zwrotu książki?
  • Ile książek było dostępnych 6 czerwca 2017 roku?

O ile w przypadku pierwszych trzech pytań jestem w stanie uwierzyć, że te informacje są mniej lub bardzie trudne do wyciągnięcia z innych tabel, o tyle ostatnie jest dość kłopotliwe. Bazuje ono bowiem na mierze, która nie jest wpisana w obecną strukturę danych – czasie.

I tu pojawia się wspomniany wcześniej Event Sourcing. Założenie jest bardzo proste. Może zamiast trzymać książkę w bazie danych pod postacią wiersza w tabelce, powinniśmy składować ją jako zbiór odpowiednio opisanych zdarzeń, które w jakiś sposób modyfikowały nasz obiekt w czasie? Zdarzenia jak i poziom ich szczegółowości, zależą już od nas i naszych potrzeb. Przykładowo mogą to być eventy:

  • Utworzono książkę
  • Zmieniono cenę książki
  • Nałożono rabat
  • Uzupełniono stan magazywnowy
  • Zmieniono opis

Kiedy będę chciał pobrać informację o konkretnej książce, wtedy ów zbiór posortuję chronologicznie, a następnie zacznę aplikować jedno zdarzenie na drugie. W ten sposób finalnie otrzymam aktualne informacje dotyczące książki. Spójrz na prosty przykład:

 

 

 

Widzimy, że książka została wprowadzona do systemu z nominalną ceną równą 80 w liczbie 10 sztuk. Następnie po dwóch miesiącach jej cena zmniejszona została do 57,99. W końcu, po roku ktoś kupił 7 sztuk tej książki. Pytanie, jakie dane uzyskamy jeśli wszystko razem złożymy do kupy?

 

 

Tak jest! Mamy nasz aktualny stan systemu, z tą różnicą, że oprócz tego posiadamy zdarzenia, które dostarczają nam wiele cennych informacji! Wiemy np. że książka nie cieszyła się wielką popularnością, ponieważ sprzedała się dopiero rok po wprowadzeniu jej do systemu. Oprócz tego właściciele księgarni zmuszeni byli do znacznej obniżki ceny, aby zachęcić ludzi do kupna. Te informacje mogą być przydatne przy bardziej zaawansowanych analizach bądź funkcjonalnościach jak np. spersonalizowane kampanie sprzedażowe. Ale to jeszcze nie wszystko (zajechało telezakupami Mango)! Pamiętasz ostatnie pytanie o ilość książek z 6 czerwca 2017 roku? Dzięki ES odpowiedź jest bardzo prosta do udzielenia. Wszystko co musimy zrobić to przefiltrować aplikowane zdarzenia po polu DateTime. Innymi słowy, weźmiemy pod uwagę wszystkie zdarzenia, które zaszły do pożądanej daty, a nowsze pominiemy. Dzięki temu będziemy z łatwością w stanie stwierdzić, że stan magazynowy wynosił wtedy… 10 książek, ponieważ żadna się jeszcze wtedy nie sprzedała.

 

Jeżeli uważasz to podejście za dziwne i szukasz bardziej życiowych przykładów to pomyśl o swoim koncie w banku. Czy aktualne saldo jest na pewno tylko polem liczbowym w tabelce Accounts? Uważasz, że tak poważna organizacja jak bank przechowywałaby fortuny pod postacią jednej liczby? Co w przypadku błędu w logice aplikacji, która zmodyfikowałaby ją niepoprawnie? Co w przypadku, współbieżnych dostępów do bazy danych? A może… może saldo jest skalarem wyliczanym na podstawie wszystkich Twoich transakcji, które wykonałeś na danym koncie bankowym 😉

 

Wszystko to wygląda jak rozwiązanie idealne, które powinno być zawsze domyślnym w każdym projekcie IT, prawda? Jeżeli mamy wybierać między rozwiązaniem typowym, a Event Sourcingiem, dzięki któremu otrzymam dostęp do cennych informacji dodatkowych, to wybór jest oczywisty. Niestety wielka moc i w tym przypadku niesie za sobą wielką odpowiedzialność. Odpowiedzialność za utrzymanie wydajności w naszym systemie. Nietrudno zauważyć, że w klasycznym podejściu pobranie danych jest bardzo łatwe. Wszystko sprowadza się do prostego SELECT-a z filtrem na Id i gotowe. Mamy pobraną książkę. W przypadku ES sprawa jest nieco bardziej skomplikowana bo najpierw musimy pobrać N zdarzeń, a następnie zaaplikować je kolejno aby uzyskać aktualny stan obiektu. To jednak relatywnie mały problem. Gorzej zaczyna się robić w przypadku gdy chcielibyśmy filtrować np. po cenie. W przypadku klasycznego SQL-a jest to znów dziecinnie proste. W przypadku składowania danych jako zdarzenia mamy nie lada wyzwanie. Dlaczego? Cena jest czymś co może być (i w naszym przypadku było) zmienne w czasie. Jeżeli ustawię filtr, który ma pobrać książki z ceną powyżej 70 to, aby stwierdzić czy na dzień dzisiejszy „C# in depth” powinno się w kolekcji wynikowej znaleźć, muszę pobrać wszystkie eventy, odtworzyć stan obiektu i sprawdzić jego cenę. Daj nam to znów N eventów, ale tym razem pomnożone przez K książek, które znajdują się w bazie danych. Nie mamy możliwości przefiltrowania inaczej jak sprawdzenia wszystkich. I tu pojawia się problem związanych z Event Sourcingiem. Sama struktura danych z definicji nie jest zoptymalizowana pod odczyt. Potrzebujemy czegoś co pozwoli nam cieszyć z dobrodziejstw ES, a jednocześnie zapewni nam wydajność przy odczycie danych. Potrzebujemy CQRS.

 

Command Query Responsibility Segregation

CQRS jest rozwinięciem starego konceptu o nazwie CQS (Command Query Separation) przedstawionego przez Betranda Mayera w 1986 roku. Stwierdził on wtedy, że metody w naszym systemie powinny przynależeć do jednej z dwóch grup:

  • Commands (pol. komendy) – metody, które posiadają „side effects” (jak mutowanie stanu aplikacji), ale nic nie zwracają (void).
  • Queries (pol. kwerendy) – metody, które zwracają dane, ale nie posiadają „side effects”. Są to zatem czyste funkcje (ang. pure functions).

Tylko tyle i zarazem aż tyle. Nie staraj się na siłę łączyć operacji zapisu i odczytu danych (tworząc metody typu GetOrCreate), ponieważ są one całkowicie różne. Zapis danych charakteryzuje się zazwyczaj jakąś logiką biznesową, która w mniej lub bardziej udany sposób opisuje reguły kreacji obiektów. Jest to „serce” i niejako istota naszego systemu/aplikacji. Odczyt jest tylko „głupią” operacją, która nie powinna się charakteryzować żadną dodatkową logiką. System odpytany 100 razy o tą samą rzecz powinien zwrócić zawsze ten sam rezultat.

Kiedy zaczniesz stosować CQS, szybko zauważysz jak pozytywnie potrafi wpłynąc na Twój kod. Poniżej kilka wybranych plusów:

  • Łatwiejsza analiza kodu – podział metody na dwie mniejsze, pozytywanie wpływa na ewentualne analizowanie kodu w przyszłości. Mniejsza ilość linii to mniej kodu, który trzeba zrozumieć w ramach jednej metody. Poza tym unikniesz pułapek, które w przyszłości mogą Tobie lub Twoim kolegom, odebrać chęć życia przy debugowaniu. Mowa tu np. o getterach, które w środku posiadają jakieś „side effects”. Oprócz tego, przy szukaniu błędu w logice biznesowej, możesz „z marszu” odrzucić wszystkie metody, które jedynie pobierają dane, ponieważ zgodnie z CQS nie zmieniają stanu aplikacji.
  • Łatwiejsze testowanie – w jaki sposób napisałbyś testy jednostkowe dla metody typu GetOrCreate? Bez znania aktualnego stanu domeny nie jesteśmy w stanie jasno stweirdzić czy w danym przypadku wykona się część Get czy Create. Co więc zatem zrobić w teście? Dodać LOGIKĘ, która będzie odpowiednio sterowała wywołaniem metody? A może posilić się testami kombinatorycznymi, które sprawdzą N wywołań metody dla różnych parametrów? Musisz przyznać, że nie brzmi to zachęcająco. Stosując CQS w bardzo prosty sposób się od tego odcinamy. Dlaczego? Każda metoda otrzyma swoje dedykowane testy, które sprawdzą poprawne jej działanie. Ponadto kwerendy są banalnie łatwe do testowania, ponieważ są czystymi funkcjami. Dając ten sam argument zawsze otrzymamy ten sam rezultat.
  • Częściowa idempotentność – Właściwość przedstawiona w ostatnim zdaniu, poprzedniego podpunktu ma swoją naukową nazwę – idempotentność. Możliwość wywoływania funkcji N razy z gwarancją otrzymania identycznego rezultatu ma olbrzymie znaczenie w kontekście systemów rozproszonych. Wkrótce do tego dojdziemy 😉 Tak wiec, stosowanie CQS daje nam za darmo część metod (kwerendy), które z definicji są idempotentne. W przypadku miksowania metod możemy (choc nie musimy) miec ich okrągłe zero.

 

Na początku tego paragrafu napisałem, że CQRS jest rozwinięciem omówionego CQS. Na czym ów rozwinięcie polega? Dokonałeś sepracji metod na komendy i kwerendy? Super! Teraz dokonaj ich segregacji wydzielając je do dwóch odzielnych obiektów o róznych odpowiedzialnościach (znów odczyt/zapis). To wszystko. CQRS to nic innego jak CQS na poziomie obiektów, a nie metod. Niesamowicie Entrprise-owy wzorzec, prawda? Sam Greg Young (twórca CQRS) przyznał:

 

„CQRS and its core is probably the dumbest pattern ever imagined”

 

Większość wzorców, które znajdują się w kursach czy książkach wymagają więcej uwagi i zrozumienia. Skąd zatem obawa przed jego rzekomym skomplikowaniem? Wróćmy na chwilę do Event Sourcingu i problemu z jakim pozostaliśmy. Nasza struktura danych, która przetrzymuje zdarzenia nie jest zoptymalizowana pod kątem bardziej skomplikowanych zapytań. W jaki sposób CQRS może nam pomóc? Skoro chcemy traktować odczyt i zapis jako dwa osobne byty, to może warto przenieść to również na poziom infrastruktury i pomysleć o dwóch bazach danych? Aktulną bazę ze zdarzeniemi uczynilibyśmy bazą WRITE, z której pobieralibyśmy dane tylko do odtworzenia stanu jednego obiektu tak, aby wykonać na nim jakąś logikę biznesową. Jednocześnie obok posiadalibyśmy bazę READ, która synchronizowałaby się z bazą WRITE, ale dane w niej zawarte ułożone byłyby w strukturach przeznaczonych stricte pod odczyt np. zdenormalizowane tabele SQL. Co więcej, kto mówi o tylko jednej bazie do odczytu? Jeżli mamy wielu konsumentów naszych danych, może się zdarzyć, że każdy chciałby widzieć je w nieco innej formie. Możemy zatem pokusić się kilka baz, w różnych technologiach, z różnymi strukturami danych.

Teraz pytanie, czy ten ewidentny poziom skomplikowania jak np. wiele baz danych jest implikacją wprowadzenia CQRS? Moim zdaniem nie. To ES wprowadził komplikację, która musiała zostać w jakiś sposób zażegnana. CQRS powiedział tylko „hej, kto powiedział, że musisz używać tej samej formy danych do zapisu i odczytu?”. To wszystko. Stąd moim zdaniem tak wielkie obawy przed tym czteroliterowym akronimem. Programiści nie zdają sobie sprawy z poniżej zależności:

 

 

Mam nadzieję, że teraz wszystko jest już jasne. CQRS to nie olbrzymia architekura klasy Enterprise, a raczej „good practise”, który może wspomóc cięższe podjeścia jak ES. Nic nie stoi jednak na przeszkodzie, aby wprowadzić go „stand alone” tam gdzie uznasz to za zasadne. Nasuwa się jedynie pytanie czy i w tym przypadku rozwiązanie z pozoru idealne niesie za sobą jakieś negatywne implikacje, szczególnie w kontekście cyklu o mikroseriwsach? Dowiesz się tego wkrótce… stay tuned.

 

You may also like...

  • Pingback: dotnetomaniak.pl()

  • Random Guy

    Pure function to troche wiecej niz funkcja bez ”side effects”.
    Przyklad:
    int GetRandom() => new Random(100).GetNext();
    nie jest pure function

    Poza tym, artykul bardzo przyjemny do przeczytania. Czekam na kolejny

    • Racja, dopiero potem piszę o idempotentności co nie dla wszystkich może być jasne. Dzięki za trafną uwage!

      • Arek Bal

        Myślę że dyscyplina pamięci transakcyjnej opisuje to najlepiej.
        Nie wchodząc w definicje… jeśli masz same odczyty w kolejce to mogą zwrócić to samo – tzn. moźna wyliczyć kwerendę raz i na wszystkie zapytania odpowiedzieć tym wynikiem z cache. Memoization takie. Odpalenie komendy – operacji write – będzie skutkowało unieważnieniem cache. W zależności też jak to zaprojektujemy (i jak często mamy writy) możemy też priorytetyzować writy w ten sposób byśmy mieli zawsze możliwie najaktualniejsze dane w readach.

  • Łukasz Ledóchowski

    Event sourcing nie wymaga CQRS, tak samo jak CQRS nie wymaga event sourcingu. To są dwa osobne koncepty, które często są widziane razem. Poza tym przykład z zapytaniem SQL dziwny. W event sourcingu występują projekcje, których efektem mogą być dokładnie takie same struktury danych jak w „tradycyjnym” podejściu SQL, więc akurat problemu z odpytywaniem nie widzę. Problem polega na tym, że mamy event store i projekcje, a np. częste zmiany koncepcji powodują, że musimy brać pod uwagę całą historię systemu, a nie tylko stan obecny. Nie spotkałem się z tym, aby ktoś używał samego event store, bez jakiegoś mechanizmu projekcji.

    • Dziękuję za komentarz!
      Odpowiem trochę nie po kolei. Jeśli chodzi o projekcje to nie pisałem o nich wprost, ale miałem je na myśli chociażby we fragmencie „Kiedy będę chciał pobrać informację o konkretnej książce, wtedy ów zbiór posortuję chronologicznie, a następnie zacznę aplikować jedno zdarzenie na drugie.”. Zgadzam się, że ich wynikiem mogą być struktury danych jak w tradycyjnym podejściu relacyjnym, stąd zduplikowany obrazek z rekordem SQL po pytaniu „Pytanie, jakie dane uzyskamy jeśli wszystko razem złożymy do kupy?”. Odnosząc się do samego mechnizmu projekcji to nie twierdzę, że jest to problem, aby odpytać bazę. Bardziej poddaję pod wątpliwość wydajność przy wyciąganiu jakiegoś skomplikowanego raportu w porównaniu do typowego zapytania SQL. Po prostu projekcja, nawet jeśli dostarczana przez event store, musi operować na strumieniu zdarzeń. Są co prawda mechanizmy poprawiające wydajność jak snapshoty, ale to dalej jakaś dodatkowa praca z odtworzeniem obiektu niż prosty SELECT. Co do powodu ze zmianą koncepcji to nie jestem pewien co masz konkretnie na myśli. Jeśli chodzi o zmianę struktury samych eventów to nikt nie broni używania NoSQL, które z definicji są schema less (jest jeszcze co prawda implicit schema, ale to już kwestia podejścia do projektowania DAL). W przypadku gdy event sotre jest oparty o relacyjną bazę danych to dalej można pokusić się o trzymanie ich jako JSON i uniknąć problemu z ich kolejnymi wersjami. Taki support ma już od dawna PostreSQL czy MS SQL Server. Jeżeli miałeś coś innego na myśli pod hasłem „zmiany koncepcji” to daj proszę znać 🙂 Odnośnie ostatniego obrazka to odpowiem w ten sposób, ES w teorii nie wymaga CQRS bo jak obaj twierdzimy są to osobne koncepty adresujące inne problemy. Uważam jednak, że sama struktura danych i sam koncept modularyzacji obiektu na eventy powoduje, że nie jest to forma przyjazna do odczytu. Stąd jeśli nie chcemy mieć problemów stricte wydajnościowych to niejako jesteśmy zobligowani to rozbudowy naszego systemu w oparciu o CQRS. Sam obrazek wyrwany z kontekstu może trochę budzić kontrowersje, ale myślę, że w kontekście tego co napisałem wcześniej jest poprawny.

      • Łukasz Ledóchowski

        Jeżeli odpytujemy o dane, to odpytujemy dla bieżącego stanu projekcji. W momencie tworzenia raportu mamy określony stan projekcji, w żaden sposób jej nie odbudowujemy, ani nie sięgamy do event store. Projekcje aktualizowane są na bieżąco, na podstawie przychodzących eventów. Tak działają systemy oparte o CQRS, które znam. Nie ma tu problemu K*N. Oczywiście potrafię wyobrazić sobie system, w którym projekcje nie są aktualizowane na bieżąco, a dogrywane są tylko wtedy, gdy nastąpi zapytanie o dane, ale to jest słabe rozwiązanie.

        „Potrzebujemy czegoś co pozwoli nam cieszyć z dobrodziejstw ES, a jednocześnie zapewni nam wydajność przy odczycie danych. Potrzebujemy CQRS.” – CQRS w żaden sposób nie odnosi się do event store, więc nie widzę, aby CQRS był tutaj rozwiązaniem czegokolwiek. CQRS to prosty (w teorii) podział systemu na dwie części. CQRS nie wprowadza pojęcia wydajnych projekcji, ani nic podobnego. CQRS nie mówi jak przetwarzać dane w event store, aby były wygodniejsze do odczytu.

        Hasło „zmiana koncepcji” podam na przykładzie. Mamy event, w którym trzymamy np. jednostkę pozycji na fakturze. Pierwotnie pozwalaliśmy użytkownikowi wpisywać co chce, ale okazało się, że musimy zastąpić to pole słownikiem predefiniowanych jednostek, bo użytkownicy robią śmietnik, używając czasami dużych, czasami małych liter, a czasami wpisując kompletne głupoty. W historii zdarzeń do końca świata będziemy mieli dziwne wartości, a w nowych eventach będą już prawidłowo identyfikatory z zewnętrznego słownika. Ale w projekcjach i aggregate rootach musimy użerać się i z tym, i z tym. Nie zapomnimy nigdy, że na początku projektowania systemu zrobiliśmy błąd w założeniach. Możemy oczywiście przepisywać zdarzenia w event store i zmienić historię, ale to słabe podejście, bo dla wielu event store powinien być niezmienny. To jest jeden z większych problemów tego podejścia.

        • Zmiana koncepcji – teraz wszystko jest jasne i zgadzam się w 100%. Event store raczej z założenia jest append only więc jakaś zewnętrzna ingerencja z ewentualnymi korektami danych jest słaba. Problem z utrzymaniem wszystkich wersji zdarzeń wewnątrz agregatów też już mi się wyklarował. Dzięki za doprecyzowanie.
          Odnośnie projekcji to widzę po prostu różnicę w doświadczeniu (oczywiście na Twoją korzyść). Ja ES stosowałem 2 razy: raz w swoim prywatnym projekcie, żeby obyć się z konceptem i 2 raz w małym projekcie „komercyjnym” jednak tam użycie było bardzo proste i cały mechaznim pisałem sam, nie posiłukąc się np. EventStore. Stąd dla mnie projekcje były stricte „on demand”. Odnośnie tego co napisałeś „Tak działają systemy oparte o CQRS, które znam.” to płynnie przejdę do pierwszej części Twojego komenatrza.
          Nigdzie w tekście nie padło stwierdzenie, że CQRS jest wzorcem projektowym dzięki, któremu nasza aplikacja będzie wydajna. Od początku też zwracałem uwagę, że CQRS i ES są kompletnie ze sobą nie powiązane. Uważam jednak, że w kontekście ES (konkretniej w kontekście event store) wprowadzenie CQRS może POŚREDNIO pozytwynie wpłynąć na wydajność. Nie dlatego, że ten wzorzec wprowadza jakies magicznie wydajne struktury, mechnizmy itp. tylko dlatego, że poniekąd otwiera oczy programiście mówiąc „hej, nie musisz męczyć się z tym samym modelem przy odczycie”. Zgadzam się, że ostatnie dwa zdania części o ES, które wkleiłeś mogą sugerować co innego, ale uważam też, że po przeczytaniu części o CQRS wszystko staję się jasne.