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...