CQRS - wzorzec, który w ostatnim czasie znaleźć można w większości nowych projektów. Buzzword świata IT, który widnieje w czołówce obok mikroserwisów i DDD. Wzorzec tak popularny, a jednoczenie w wielu przypadkach tak błędnie rozumiany. Definicja wydaje się być całkiem prosta i dość zrozumiała. Separacja komend i kwerend. Ale co właściwie z tego wynika? Czy implementacja tego wzorca w prawdziwym projekcie jest tak trudna jak mówią? Te kwestie poruszę w krótkiej serii gdzie w ramach kilku postów przedstawię jak można podjeść do tematu na kilka różnych sposobów.

Studium przypadku

Modelowanie nietrywialnych domen jest prawdziwym wyzwaniem. Popełnić błąd można bardzo szybko, a niejawny coupling kryje się na każdym korku. Pomocną dłoń wyciągają pewne techniki, które dostarczają szereg taktycznych i strategicznych wzorców. Warto z nich skorzystać aby choć trochę zwiększyć swoje szanse w tej nierównej walce przy poznawaniu coraz to subtelniejszych szczegółów domeny. Podążając za nimi i wykorzystując paradygmaty programowania obiektowego, pierwszym krokiem jest porzucenie reprezentacji obiektów przy pomocy zestawu pól i zastąpienie ich zachowaniami - enkapsulacja pól i szczegółów implementacyjnych. Uzyskany w ten sposób interfejs przedstawia jedynie zachowania. Ten radosny triumf okiełznania domeny zostaje przerwany, gdy idealnie przygotowany model trzeba przedstawić użytkownikowi. A właściwie nie model, ale zestaw pól, które model ten opisują. To, co wcześniej zostało ukryte za głęboką warstwą abstrakcji - szczegóły implementacyjne. Sam model, co prawda, użytkownika nie interesuje. Dla biznesu liczy się wynik. Problem jest dość poważny, bo podstawowe potrzeby biznesu nie zostały zapewnione. Rozwiązanie jest proste - wystawienie pól na zewnątrz, zrobienie gettera. Ale czy jest to słuszne posunięcie? Przecież udostępnienie szczegółów implementacyjnych powoduje zepsucie idealnego modelu, który tak długo był dopieszczany. Może jest inny sposób? Otóż jest. Sytuację ratuje CQRS.

Od czego się wszystko zaczęło?

Jako programistom, zostało nam wbite do głowy, że podczas projektowania obiektów dobrą praktyką jest wydzielenie metod, które modyfikują stan obiektu oraz tych, które stan ten zwracają. Taki podział daje swego rodzaju poczucie bezpieczeństwa. Podświadome przeczucie, że samo pobranie danych nie powinno niczego zepsuć. Obserwacje te spisał jako pierwszy Bertrand Mayer, który w swojej kultowej książce “Object-Oriented Software Construction” [1], wydanej w 1988 roku, przedstawił je jako CQS - Command Query Separation. Zaproponował wydzielenie Commandów, które modyfikują stan oraz Queriesów, które są wolne od side-effectów. Właśnie z tego podziału wzięło się powiedzenie, że zadanie pytania nie powinno zmieniać odpowiedzi. Do każdej reguły znajdziemy jednak wyjątek. Dobrym przykładem jest znana struktura danych - stos [2]. Mowa tu o metodzie pop. Choć można sobie wyobrazić rozdzielenie operacji odczytania wartości pierwszego elementu i jego usunięcie, to wykonanie tych operacji atomowo jest znacznie bardziej funkcjonalne. Łatwo przewidzieć jak taki niefortunny podział by się zakończył. Prędzej czy później inna wartość została by odczytana, a inna usunięta. Nasuwa się więc pewien wniosek: podział na Commandy i Queriesy warto stosować przy modelowaniu zachowań. Struktury danych rządzą się natomiast swoimi prawami. Tu taki podział, może przynieść więcej szkód niż korzyści. Trzeba jednak pamiętać, że każdy przypadek jest inny, dlatego tak ważne jest pragmatyczne podejście, a nie ślepe podążanie utartymi schematami.

Ewolucja z CQS do CQRS

Kilkanaście lat temu, Greg Young zauważył, że do realizacji logiki biznesowej wygodnie jest zamodelować zachowania oraz dodać do nich wszystkie zawiłe reguły domeny. Jednakże, skutki działania tego modelu są niebywale proste. Wyniki mogą zostać zaprezentowane przy pomocy prostych danych ilościowych. Projekcji tego wszystkiego, co się wydarzyło z pominięciem reguł na podstawie których wyniki te powstały.

Jak pogodzić te różne potrzeby?

Wniosek nasuwa się sam: model domenowy różni się od modelu, który finalnie interesuje użytkownika. Ba, modele te realizują zupełnie inne potrzeby. Próba wyrażenia jednego modelu drugim jest dość karkołomna i może prowadzić do wielu problemów. Albo skończymy z modelem zapisowym, który zwierać będzie szereg informacji potrzebnych do odczytu, jak sklejanie stringów czy innych dziwnych rzeczy odczytowych, albo z modelem, który odwzorowuje strukturę bazodanową. Aby temu zaradzić, Greg Young zaproponował wzorzec, który nazwał Command Query Responsibility Segregation. Segregację odczytu i zapisu. Rozszerzył znany i dobrze przyjęty CQS o podział Commandów i Queriesów na osobne byty poprzez wprowadzenie dla nich dedykowanych modeli. Rozdzielając te modele znacznie prościej jest zapewnić czysty, zrozumiały i testowalny kod. Przy ich pomocy można realizacji jedno zadanie do którego zostały stworzone, co pomaga zachować SRP. Krótko mówiąc, CQRS zwiększa wydajność zarówno programisty, bo ile łatwiej jest modyfikować czysty i zrozumiały kod oraz daje potencjalne szanse na zysk wydajnościowy oraz skalowalność. Kwestie te zostawiam jednak na później.

To właściwie tyle w temacie- idea jest niebywale prosta. Segregacja zapisu i odczytu, czyli stworzenie dedykowanych dla nich modeli. Cała dodatkowa otoczka, która bardzo często występuje, jak Event Sourcing, Eventual Consistency czy sto pięćdziesiąt Cassandr do szybkich odczytów nie ma znaczenia. Nie o to tu chodzi. Prawdą jest to, że wszystkie te rzeczy (no może z wyjątkiem takiej ilości Cassandr, bo w końcu nie każdy ma skalę Netlixa czy Spotify) często występują razem, ale wcale nie są jego nieodłączną częścią. Separacja modeli nie musi wcale oznaczać oddzielnych baz danych. W dużej części przypadków wystarczy jedna, zwykła relacyjna baza danych.

Kiedy nie warto korzystać z CQRS?

Jak ze wszystkim, CQRS wepchnąć można dosłownie wszędzie, ale nie wszędzie będzie on pasować. Weźmy na przykład aplikację, której zadaniami jest umożliwić subskrypcję newslettera oraz wyświetlić wszystkich zasubskrybowanych użytkowników.

Przykładowy Command mogłaby wyglądać następująco:

public class SubscribeToNewsletter : ICommand
{
    public string Name { get; set; }
    public string Email { get; set; }
}

Natomiast Query i model, który ono zwraca w ten sposób:

public class SubscriberDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

public class RetrieveSubscribers : IQuery<IEnumerable<SubscriberDto>>
{
}

Otrzymane modele wyglądają właściwie identycznie, z dokładnością do identyfikatora. Te same dane są wykorzystywane do utworzenia subskrypcji a następnie te same dane są pobierane do wyświetlenia wszystkich subskrybentów. Po co więc komplikować? Tu wystarczy jeden, prosty model subskrybenta, zaprezentowany w klasie SubscriberDto.

Jasne, przykład jest maksymalnie uproszczony. Chcę jednak pokazać, że cześć aplikacji po wprowadzeniu takiej separacji, zawierać będzie bliźniaczo podobne modele. Są to aplikacje klasy CRUD, często nazywane “przeglądarkami bazy danych”. Zapis, odczyt, modyfikacja, czasem usuwanie - te cztery operacje to wszystko, co oferują użytkownikowi, cała logika. Inwestycja w CQRS nie jest szczególnie opłacalna. Mogę się założyć, że tylko wszystko utrudni. Dodatkowy narzut implementacyjny i zwiększony poziom skomplikowania wydłużą development. W tym przypadku architektura typu “encja na twarz i pchasz” sprawdzi się idealnie.

W takim razie kiedy?

Rozbudowane aplikacje, z bogatą domeną i skomplikowanymi regułami, które projektowane są w podejściu Domain-Driven działają trochę inaczej. Interfejs użytkownika przedstawia precyzyjnie dobrane widoki, a model domenowy - czyli zapisowy - jest ukryty w sercu aplikacji. Wszelkie zmiany stanów dokonywane są przez starannie dobrane polecenia [3], które można wywołać z poziomu UI. W ten i tylko ten sposób. Prezentowane widoki natomiast, to zazwyczaj połączeniem wielu różnych encji, a niekiedy nawet różnych systemów. Pozbycie się części odczytowej z modeli domenowych pozwala uprościć kod i oczyścić model z rzeczy, które nie są częścią encji i agregatów. A jeżeli w modelu odczytowym będą potrzebne, to zostaną tam dodane. Wtedy i tylko wtedy. Całkowita niezależność. Ograniczeniem jest tylko model odczytowy. Model, który sami przygotujemy.

Rozważmy poprzedni przykład z zapisem do newslettera, jednakże rozszerzony o dodatkowe założenia.

  • Użytkownik zapisuje się do newslettera w sklepie internetowym.
  • Za subskrypcję użytkownik dostaje kod rabatowy.
  • Sklep internetowy pozwala na korzystanie z kodów rabatowych
  • System umożliwia wyświetlenie tych zamówień, które zostały zrealizowane z wykorzystaniem kodu rabatowego pochodzącego z zapisu do newslettera.

W podejściu Task Based UI będą to dwa polecenia:

  • Zasubskrybuj newsletter
  • Złóż zamówienie

Ostatni punkt “wymagań biznesowych” może sprawić trochę problemów. Trzeba tu jakoś połączyć newsletter z zamówieniem. W klasycznym podejściu znaczyłoby to, że albo newsletter trzyma informacje o zamówieniu, albo zamówienie przechowuje informacje o zapisie do newslettera. Wychodzi na to, że któraś z encji będzie zawierać dane, których właściwie nie potrzebuje. Trochę dziwnie trzymać informacje o newsletterze w zamówieniu, nieprawdaż?

Aby zachować czystą domenę, wystarczy przygotować dedykowany model odczytowy, który połączy newsletter z zamówieniem przy pomocy wygenerowanego kodu rabatowego. Widok gotowy, a modele zapisowe pozostają maksymalnie proste. Wilk syty i owca cała.

Czy tylko w takich przypadkach?

Poza systemami ze skomplikowanymi regułami, przesłanki sugerujące skorzystanie z tego wzorca mogą mieć czysto wydajnościowe podłoże. Na szybko przychodzi mi wykorzystanie silnika Elasticsearch do wyszukiwania. Odpowiednio przygotowany model odczytowy, specjalny silnik do wyszukiwania i możliwości niezależnego skalowania. Brzmi fajnie, a baza danych oraz użytkownicy końcowi nam podziękują.

W tym miejscu chciałbym zakończyć. Ten wpis jest czysto teoretyczny i miał na celu wprowadzenie w temat. W kolejnym przejdę do części praktycznej i pokażę jak bardzo łatwo wprowadzić CQRS do projektu z wykorzystaniem jednej bazy danych.

[1] Meyer Bertrand, (1988, 1997). Object-Oriented Software Construction. ISBN 0-13-629155-4 (1997 ed.)

[2] Fowler Martin, (2005, 5 grudnia). CommandQuerySeparation [wpis na blogu]. Źródło: https://martinfowler.com/bliki/CommandQuerySeparation.html

[3] Greg Young. CQRS Documents by Greg Young. Rozdział: Task Based User Interface Źródło: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Zostaw komentarz