CQRS/ES #1 Trochę teorii

Jakiś czas temu zapowiadałem na blogu serię postów poświęconą tematyce CQRS oraz Event Sourcing-u. Niniejszym postem rozpoczynamy naszą podróż badawczą! Dlaczego w ogóle zajmiemy się tym tematem? Otóż sam koncept poznałem stosunkowo niedawno i pomijając wady i zalety tego rozwiązania zawsze spotykałem się ze stwierdzeniem, że implementacja CQRS jest banalna i nie ma nad czym się tu zastanawiać. Już teraz mogę Wam zdradzić, że nie zgadzam się z tą tezą, ale do kodu przejdziemy od następnego „odcinka”. Tu jeszcze chciałbym zachęcić Was do komentowania moich poczynań. Jeżeli uważacie, że coś można zrobić inaczej/lepiej/szybciej to piszcie! Skorzystam na tym ja – laik CQRS – oraz inne osoby, które kiedyś tu trafią. Po tym przydługim wstępie możemy w końcu przejść do rzeczy :)

 

CQS – Command Query Separation

Zanim przejdziemy do bohatera pierwszoplanowego dzisiejszego wpisu (CQRS), warto zapoznać się z konceptem, z którego bezpośrednio się on wywodzi. Mowa o CQS, czyli Command Query Separation. Został on przedstawiony w roku 1986 przez  Bertranda Meyera. Widać więc wyraźnie, że wbrew powszechnemu stwierdzeniu nie jest to nic nowego. Czym zatem jest CQS? Jest to zasada, która mówi że każda metoda w systemie powinna być zaklasyfikowana do jednej z dwóch grup:

  • Command – są to metody, które zmieniają stan aplikacji i nic nie zwracają.
  • Query – są to metody, które coś zwracają, ale nie zmieniają stanu aplikacji.

 

Bardzo spodobało mi się zdanie, które dobrze tłumaczy tą ideę:

 

Pytanie nie powinno zmieniać odpowiedzi.

 

Może wydawać się to dość oczywiste, jednak z mojego doświadczenia wiem, że programiści nie zawsze się do tego stosują. Bardzo prostym przykładem są metody, które wyglądają następująco:

 


public Item GetOrCreateItem(ItemModel model)
{
    //logika metody
}

 

Widać tu mały problem. Metoda GetOrCreateItem nie zawsze będzie się zachowywać identycznie. Poza tym mieszamy kod logiki biznesowej aplikacji z „głupim” pobraniem obiektu z bazy danych. Niesie to za sobą pewne problemy, o których wspomnę nieco później. Jak zapewne się domyślacie stosując CQS nasz kod w takim przypadku wyglądał by następująco:

 

//Query
public Item GetItem(ItemGetModel model)
{
    //logika metody
}


//Command
public void CreateItem(ItemCreateModel model)
{
    //logika metody
}


 

CQRS – Command Query Responsibility Segregation

Blisko 20 lat po „narodzinach” CQS, dwie wielkie osobistości tj. Greg Young oraz Udi Dahan przedstawili światu jego następce czyli CQRS – Command Query Responsibility Segregation. Pomysł był bardzo prosty. Dlaczego dokonujemy podziału jedynie metod na te, które pobierają dane oraz na te, które zmieniają stan naszej aplikacji? Możemy przecież zaprojektować nasz system tak, aby tymi zadaniami zajmowały się osobne klasy. To jest główna różnica między dwoma podejściami.

 

Mówiąc o CQS myślimy o metodach. Mówiąc o CQRS myślimy o obiektach.

 

W tym miejscu należy dodać, że nie ma jednej drogi do zaimplementowania CQRS, ponieważ tak jak w przypadku każdego wzorca projektowego podejść jest kilka. Zaraz, zaraz! Wzorca? Tak, wbrew wielu opinii, które możecie znaleźć w internecie CQRS nie jest architekturą aplikacji. To od nas zależy czy zostanie on wprowadzony „globalnie”, czy jedynie w niewielkim jej fragmencie. Potwierdził to sam Greg Young:

 

CQRS and Event Sourcing are not architectural styles. Service Oriented Architecture, Event Driven Architecture are examples of architectural styles.

 

Dobrze, przejdźmy więc do graficznej reprezentacji ów wzorca, która powinna pomóc nam zrozumieć z czym tak na prawdę mamy do czynienia:

 

CQRS1

 

Tak jak wspomniałem jest to jedna z możliwych implementacji. Na schemacie widzimy wiele składowych, dlatego przejdźmy do omówienia ich po kolei:

 

  • Command – jest to obiekt, który reprezentuje intencje użytkownika systemu. Przykładowo możemy stworzyć obiekt UpdateItemQuantityCommand z polami Id oraz Quantity.
  • Command Bus – ma dwa zadania. Po pierwsze zapewnia kolejkowanie wszystkich wchodzących do systemu komend. Po drugie, odszukuje odpowiedni dla danej komendy Command Handler i wywołuje na nim metodę Handle.
  • Command Handler – jego zadaniem jest najpierw walidacja komendy. Następnie tworzy on lub zmienia stan obiektu domenowego. Ostatnim etapem jest zapisanie zmian do bazy danych (write) przy użyciu repozytorium i przekazanie wygenerowanych zdarzeń dla Event Bus-a.
  • Domain objects (models) – są sercem naszej aplikacji. To w nich znajduje się złożoność biznesowa naszego systemu. Warto zwrócić uwagę na to, że na schemacie otacza je jeszcze jedna warstwa, czyli tzw. Aggragates. Jest to wzorzec wywodzący się z Domain-Driven-Design. W dużym uproszczeniu, agregaty mają na celu traktowanie grupy logicznie/biznesowo powiązanych ze sobą obiektów jako jedną jednostkę. Dobrze przedstawił to Martin Fowler, który za przykład podał zamówienie oraz pozycje zamówienia. Obie te grupy teoretycznie mogą istnieć osobno, ale wygodniej jest to traktować jako jedna, spójna całość. Warto także dodać, że zmiany na obiektach domenowych generują w nich zdarzenia.
  • Event – jest to obiekt reprezentujący zmiany, które zaszły w systemie. Przykładowo, konsekwencją obsłużenia przedstawionej wcześniej komendy mogłoby być zdarzenie ItemQuantityUpdatedEvent.
  • Event Bus – ma dwa zadania. Po pierwsze zapewnia kolejkowanie wszystkich wygenerowanych w systemie zdarzeń. Po drugie, odszukuje odpowiedni dla danego zdarzenia Event Handler i wywołuje na nim metodę Handle.
  • Event Handler – jego zadaniem jest zapisanie zmian do bazy danych, która służy do odczytu.
  • Read Database Abstraction – jest to nic innego jak warstwa, która pośredniczy w pobieraniu danych. Sposób implementacji jest tutaj dowolny, dlatego sama nazwa na schemacie jest bardzo ogólna.

 

Domyślam się, że części z Was całość mogła jeszcze bardziej zamotać w głowie, dlatego poniżej przygotowałem diagram sekwencji, który lepiej prezentuje flow danych w systemie. Przykład prezentuje realizację komendy UpdateItemQuantityCommand:

 

sq_diagram

 

ES – Event Sourcing

W tym wszystkim musimy jeszcze odszukać rolę Event Sourcing-u. Czy jest on składową wzorca? Nie do końca. Faktycznie, na schemacie zdarzenia wystąpiły jako sposób synchronizacji dwóch baz danych. Są one bowiem częścią CQRS, jednak takie ich zastosowanie nie realizuje założeń Event Sourcing-u. Zadaniem ES jest bowiem odtwarzanie aktualnego stanu aplikacji (patrz obiektów domenowych) na podstawie zdarzeń składowanych w magazynie danych zwanym Event Store. Z początku może wydawać się to rozwiązaniem bezsensownym jednak takim nie jest, ponieważ na podobnej zasadzie działają chociażby systemy bankowe. Prosty przykład. W jednym z moich wpisów o poziomach izolacji przytoczyłem przykład ilustrujący zjawisko false update (jeżeli ktoś nie czytał niech zrobi pauzę i zapozna się z tym fragmentem, link macie tu). Zauważcie co spowodowało tam błąd składowanych danych. System przechowywał jedynie jedną liczbę, która informowała użytkownika o jego saldzie. Gdyby zamiast tego generowane było zdarzenie mówiące o zwiększeniu salda o konkretną kwotę, problem by nie istniał. Dlaczego? Ponieważ zamiast jednej informacji o saldzie, moglibyśmy aplikować w naszym obiekcie domenowym eventy, które stopniowo doprowadziłby nas do aktualnego stanu konta użytkownika. Ta prosta koncepcja jest zatem bardzo potężna w swym działaniu. Jak to się ma zatem do naszego aktualnego schematu wzorca CQRS? Jedyną zmianą byłoby zastąpienie Write DB magazynem Event Store. Drugą zmianą byłby sposób pobieranie obiektów domenowych. Zamiast pobierać je „w całości” teraz należy pobrać wszystkie zdarzenia, które wygenerował dany domain object, a następnie w jego wnętrzu je aplikować, tym samym otrzymując jego stan obecny. Proste? A jakie przydatne!

 

Po co to wszystko?

Wiemy zatem jak powinniśmy podejść do implementacji CQRS/ES. Nie znamy jeszcze odpowiedzi nie kluczowe pytanie: po co? Klasyczne aplikacje N-Layer wydają się przy tym bardzo proste i nie komplikują systemu do tego stopnia. Przygotowałem dla Was kilka powodów, które być może przekonają Was do tego konceptu:

 

  • Asymetryczna skalowalność – dzięki zastosowaniu dwóch źródeł danych jesteśmy w stanie wyskalować naszą aplikację w stronę odczytu lub zapisu. Ponadto takie podejście pozwala nam na projektowanie baz danych oraz dobór technologii tak, aby operacje odczytu/zapisu wykonywane były maksymalnie szybko. NoSQL? Zdenormalizowana baza danych? Czemu nie!
  • Podział pracy w zespole – jak zapewne zauważyliście strona odpowiedzialna za odczyt danych jest dużo prostsza od tej do zapisu. Ponadto nie realizuje ona żadnej logiki biznesowej. Daje nam to możliwość podziału prac w zespole tak, aby programiści z mniejszym doświadczeniem mogli rozwijać część odczytu, bez obawy o zmianę zachowania naszej aplikacji.
  • Mikroserwisy – ponieważ opisane wcześniej agregaty są spoiwem łączącym pewną logiczną część naszej domeny, możemy pokusić się o rozbicie naszej monolitycznej aplikacji na mniejsze części.
  • Odtworzenie stanu aplikacji z dowolnej chwili –  tak jak wspomniałem ES umożliwia nam odtwarzanie aktualnego stanu naszej aplikacji. Nic nie stoi jednak na przeszkodzie, aby zdarzenia aplikować jedynie do pewnego momentu, tym samym uzyskując stan naszej domeny z przeszłości.
  • Naturalny audyt – Implementując Event Sourcing zapewniamy sobie jednocześnie bardzo przyjemny i szczegółowy audyt naszych danych.

 

Co dalej?

Tyle teorii na dziś :) Od przyszłego posta rozpoczniemy techniczną część serii, czyli mówiąc krótko: przejdziemy do implementacji! Domyślam się, że temat nie jest łatwy do zrozumienia za pierwszym razem (przynajmniej nie był dla mnie), ale mam nadzieję, że kolejnymi wpisami rozwiejemy wspólnie wszelkie wątpliwości jak np. w jaki sposób poinformujemy użytkowników o błędzie? Żeby tego wszystkiego nie przegapić, zachęcam Was do śledzenia mnie na twitterze oraz facebooku :)

Do następnego !

You may also like...

  • Pingback: dotnetomaniak.pl()

  • Radosław Maziarka

    Nieźle zebrane :)

    • Radosław,
      dzięki! Męczyłem się z napisaniem tego wpisu, tym bardziej cieszy mnie, że jest ok :)

  • Michał Wilczyński

    Swietny artykul, tylko mam male ‚ale’. 😛
    Fajnie by bylo miec tu diagram CQRSowy bez elementow wskazujacych na ES i potem np. drugi wprowadzajacy te elementy. Co prawda ES zaproponowano obok CQRS ale to nie sa must-have dla siebie nawzajem (podobnie zreszta jak DDD).
    Mysle ze jest to o tyle wazne, ze pozwala latwiej zrozumiec idee samego CQRS (operacja jako obiekt, oddzielny read/write i modele dla command i dla query).
    Poza tym, super. 😉

    • Michał,
      myślałem nad drugim schematem, ale doszedłem do wniosku że to bez sensu. Tak jak wspomniałem we wpisie, zmianie uległaby jedynie baza danych do zapisu. W jej miejsce pojawiłby się Event Store. To co odróżnia znacznie CQRS od CQRS/ES to sposób odtwarzania stanu obiektów domenowych, czyli jest to już implementacja, której w tej części nie poruszałem. Postaram się jednak przy implementacji jakoś zaznaczyć/wyróżnić te fragmenty w kodzie, które jasno są integralną częścią ES, tak aby czytelnicy mogli sobie to wszystko poukładać. No i dzięki za pochlebny komentarz :)

      • Michał Wilczyński

        No nie do konca, bo w CQRS generalnie slowo kluczowe ‚Event’ pojawia sie gdy zaczynamy czerpac z Event Sourcingu.
        Tak naprawde CQRS nie zaklada nawet dwoch baz danych. :)

        • Nie do końca 😀 Zgadzam się co do tego, że CQRS nie wymaga dwóch baz i często faktycznie zostaje się przy jednej. Ale samo wprowadzenie eventów jako sposobu synchronizacji baz danych nie oznacza, że wchodzimy już na obszar ES. Zobacz ten fragment prezentacji: https://youtu.be/Emr4jkhW9L4?t=5m40s.

          To samo znalazło się w książce CQRS Journey „A possible implementation of the CQRS pattern uses separate data stores for the read side and the write side; each data store is optimized for the use cases it supports. Events provide the basis of a mechanism for synchronizing the changes on the write side (that result from processing commands) with the read side. If the write side raises an event whenever the state of the application changes, the read side should respond to that event and update the data that is used by its queries and views.”

          • Michał Wilczyński

            Moze rzeczywiscie troche poplynalem z tym ES ale z pewnoscia zmierza to w strone Event-Driven Architecture. :)
            Sam sie po prostu zlapalem na poczatku na tym, ze proby tlumaczenia CQRS sa zwykle ‚overengineered’ gdzie samo CQRS jest przeciez proste.

          • To prawda, sam się na tym przejechałem dość mocno 😀

  • bartek

    A ja trochę nie rozumiem:
    1. Po co tworzyć dwa oddzielne mechanizmy zapisu do bazy? To dwa razy więcej możliwości na ewentualne błędy i koszt utrzymania tego kodu jest wyższy.
    2. W jaki sposób odtworzyć stan aplikacji z dowolnej chwili? Gdy eventy przejdą, to już po jabłkach.
    3. GetOrCreateItem i tak powstanie, bo jest wygodne, tylko w innym miejscu.

    • Bartek,
      1. Nie do końca rozumiem pytanie. Owszem, powstaną dwa mechanizmy zapisu do bazy danych, ale wynika to z tego, że powstaną dwie bazy danych. W obrębie jednej bazy jest tylko jeden mechanizm.
      2. Tak jak napisałeś, zdarzenia mamy składowane w Event Store, dlatego przy pobieraniu wystarczy nam filtrowanie po dacie utworzenia zdarzenia.
      3. GetOrCreate nie powstanie. Skoro CQRS z definicji posiada podział na klasy odpowiedzialne za odczyt/zapis to taka metoda się nie pojawi. Nie do końca zgadzam się też z tym, że jest to rozwiązanie wygodne. Moim zdaniem, trochę miesza w kodzie i ja osobiście nie odczułem nigdy potrzeby takiej implementacji.

      • bartek

        1. To w takim razie nie rozumiem po co dwie bazy danych. Zawierają różne dane, czy te same?
        3. Być może mamy różne doświadczenia, ale na którymś poziome abstrakcji potrzebujesz obiektu i nie obchodzi Cię, czy on już jest w bazie czy zostanie utworzony. Np. jest post i trzeba mu przypiąć tag PROMOCJA. Musisz sprawdzić czy taki tag jest, a jeżeli go nie ma to go utworzyć i dopiero pobrać. GetOrCreate robi to za Ciebie.

        • Bartek,
          tak jak wspomniał @m_wilczynski:disqus sam CQRS nie wymaga użycia 2 baz danych, jest to pewne jego rozwinięcie. 2 bazy przydają się głównie kiedy dochodzi ES. Wtedy po stronie zapisu masz Event Store, który przetrzymuje wszystkie zdarzenia odnotowane w systemie, na podstawie których możesz odtwarzać stan obiektów domenowych. Po stronie odczytu masz dane takie jak gdybyś miał jedną bazę (te dane są updatowane na podstawie eventów, które przyszły z Event Busa). Po co dwie bazy? Tak jak napisałem, możesz skalować aplikację pod stronę zapisu/odczytu plus korzystać z technologii, które lepiej radzą sobie z zapisem i odczytem właśnie.

    • Maav

      Tutaj trzeba odróżnić CQRS (Albo nawet CQS) od Event Sourcingu.
      Twoje pierwsze dwa pytania dotyczą ESa, trzecie CQRSa.
      Jedno nie oznacza drugiego (ale bardzo często wykorzystuje się je razem).

      Ad. 1. Ponieważ możliwość odtworzenia pełnej historii ma więcej zalet niż wad (koszt utrzymania jest mniej ważny niż np. ewentualne szukanie błędów w synchronizacji danych). Event Sourcing jest skomplikowanym podejściem. Niekoniecznie uniwersalnym (w małych aplikacjach wprowadzenie ES to więcej pracy niż pożytku), ale ma swoje zastosowania (np. w bankowości).

      Ad. 3. Cały trik w CQRSie polega na tym oddzieleniu właśnie. Nie ma być wygodny w pisaniu, ale wygodny w utrzymaniu.

  • Tomek Pycia

    Kto robi takie metody/funkcje GetOrCreateItem?

    • Tomek,
      sporo programistów to popiera. Spójrz wyżej i zobaczysz, że @disqus_TkxWmz4kGu:disqus wspomniał o plusach tego rozwiązania w komentarzu 😉

      • Tomek Pycia

        Ja rozumiem ze coś można zrobić szybciej i wygodniej ale jak dla mnie taka metoda jest nie logiczna. I wprowadza bałagan w systemie i jedyny system w jakim była by dla mnie do zaakceptowania, to ten który pokazuje jak ni pisać oprogramowania. Nie jestem purystom który wszystko musi mieć zgodnie z książkami zrobione, ale za robienie funkcji GetOrCreateItem skazywał bym na kary cielesne albo na ciężkie roboty.

Śledź najnowsze wpisy!

Jeżeli treść bloga Ci się podoba, śledź mnie na twitterze lub zostaw kciuka na facebooku. To nic nie kosztuje, a pozwoli Ci być na bieżąco z najnowszymi wpisami :)