Je to už nějaký pátek, co se zajímám o vzor CQRS (Command Query Responsibility Segragation), který nachází uplatnění především u dvou typů aplikací, které ale nejsou tak časté – velmi komplexní aplikace (díky CQRS se lze lépe vypořádat s komplexitou) a velmi velké a/nebo vytížené aplikace (které potřebují škálovat). Ačkoliv se takto může zdát znalost CQRS pro běžného programátora zbytečná (ne každý je Google), je s CQRS spjato mnoho velmi zajímavých konceptů, které mohou nalézt uplatnění i u běžných aplikací. Proto jsem se rozhodl sepsat z mého hlediska to nejzajímavější, co mě CQRS naučilo.

Klasická představa vrstvení aplikace je takováto:

V krabičce „Application“ je skryta doménová logika i přístup k datům – odprostím se tedy prozatím od toho, jak je jádro aplikace implementované (DDD s ORM, Transaction Scripts, Active Record, …). Místo toho se zaměřme na to, jak je realizována komunikace mezi Aplikací a GUI – je to nějaká fasáda – vrstva rozhraní, které utváří API naší Aplikace, a nedělá tedy nic jiného, než že tupě volá Aplikaci.

Ve webovém GUI se striktně rozlišuje (a kdo to nedělá, je prasátko) mezi requesty, které provádí jen čtení (GET), a requesty jen měnícími stav (POST). Jak ví každý slušný webový vývojář, POST požadavek by neměl nic prezentovat (renderovat html) – měl by jen „nějak“ zpracovat požadovaný příkaz a říci prohlížeči, kdo mu poskytne vizuální reprezentaci výsledku (tj. na jakou stránku má udělat redirect). Tento koncept je znám jako vzor Post-Redirect-Get a vzásadě to není nic jiného než krásná implementace CQSCommand Query Separation, což je koncept, který říká, že „objekt“ by měl mít jen dva druhy metod:

  • provádí výhradně změnu a nic nevrací (tj. vrací void a nemají výstupní parametry)
  • nemění stav, jen čtou

Převedeno do řeči CQRS, GUI (obecně zbytek světa) komunikuje s Aplikací srkzeva Commands (první typ metody) & Queries (druhý typ metody):


Šipky ukazují směr toku informace.

Commands

Když chceme po Aplikaci něco vykonat (provést nějakou akci, která změní stav), pošleme směrem k Aplikaci tzv. Command. To je obyčejná třída bez metod, jen krabička na data (DTO), jejíž properties představují parametry příkazu, který chceme provést.

Commandové API Aplikace je triviální – je to jediný interface, který má jednu metodu: void HandleCommand(ICommand command)
V zájmu minimalizace requestů se často přidává druhá metoda HandleCommands, jejíž účel si laskavý čtenář jistě domyslí ;-)
Toto univerzální rozhraní má jediný úkol (často delegovaný na kontejner) – předat Command do odpovídajícího Command Handleru, který provede jeho zpracování (ten už komunikuje přímo s Aplikací).

public interface ICommandHandler<TCommand> where TCommand: ICommand
{
  void Handle(TCommand command);
}

V čem je síla takového řešení? Protože pořád nikde nic nevracíme, nemusíme čekat na „fyzické“ zpracování příkazu, tzn. GUI zobrazí obligátní hlášku „Váš požadavek bude zpracován.“ a může vesele pokračovat dál.

Informace o zpracování

Zde vyvstává otázka, jak informovat uživatele/externí systém, že byl příkaz (ne)úspěšně zpracován (pokud ho to zajímá). Protože Command Handlery nic nevrací, musíme použít jinou komunikační cestu – v případě uživatele na e-shopu nás nepřekvapí klasické poslání mailu. Jiný způsob (pokud chceme informovat uživatele interaktivněji) je ten, že ICommand obsahuje property Id typu Guid, kterou může GUI nastavit a na základě tohoto identifikátoru požadavek trackovat. Konkrétní způsoby realizace jsou mimo rozsah toho článku, ale pěkným řešením je notifikace webového serveru o zpracování Commandu skrze Message Bus a následně nějaká forma (pseudo)persistentního spojení mezi webovým serverem a prohlížečem (pollování, WebSockets, …).

Benefity

Přijde vám posílání Commandů zbytečně složité a nestojí podle vás za to, že máme díky němu možnost zadávat příkazy z GUI asynchronně? Souhlasím! Síla tohoto řešení je totiž v tom, že nám umožní rozložit zátěž v čase (tj. rozmělnit špičky v zátěži) a řešit výpadky aplikace. Těchto neocenitelných benefitů docílíme tím, že GUI nebude Commandy posílat přímo naší aplikaci, ale bude je jen předávat prostředníkovi (Message Bus, Command Bus, Message Queue), který je bude dále přeposílat naší aplikaci.
Pokud zrovna nebude naše aplikace dostupná, tak uživatel v GUI nedostane chybovou hlášku, ale požadavek (Command) se uloží do fronty, a jakmile aplikace naskočí, prostředník to zmerčí, přepošle Command aplikaci a ta ho zpracuje. Uživatel tak dostane mail s potvrzenou objednávkou maximálně o chvilku později a není obtěžován momentálních chvilkovým výpadkem aplikace.

Onen prostředník (MessageBus) by pro nás měla být (podobně jako databáze) jen infrastrukturní záležitost (100% dostupná) a typicky ji nebudeme implementovat sami, ale použijeme nějaké existují řešení – v případě vlastní infrastruktury např. MSMQ nebo RabbitMQ, v případě cloudu např. Azuří Service Bus nebo Amazoní Simple Queue Service.

Kompatibilita

Když se na tento způsob interakce GUI a Aplikace podíváme s odstupem, zjistíme, že to není o tolik složitější než klasický přístup. Místo každé hlavičky metody budeme mít třídu reprezentující Command a tělo metody přesuneme do metody Handle odpovídající Command Handleru (nic nebrání, aby jedna třída implementovala více podobných Command Handlerů).

public OldFashionedFacade: IOldFashionedFacade
{
  public void OrderProduct(Product product, Customer customer)
  {
    // handle using Application
  }
}

public class OrderProductCommand : ICommand
{
  public Product Product { get; set; }
  public Customer Customer { get; set; }
}

public class OrderProductCommandHandler: ICommandHandler<OrderProductCommand>
{
  public void Handle(OrderProductCommand command)
  {
     // handle using Application
  }
}

Za povšimnutí stojí to, že parametry staré metody přejdou v properties třídy reprezentující Command. Pokud chceme mít nadále dostupné old-fashioned API (např. si nemůžeme dovolit měnit takto zásadně API), můžeme jednoduše nagenerovat toto API z Commandů. Jednoduše pro každý Command vygenerujeme jednu metodu fasádního interface, properties Commandu překlopíme na parametry této metody a metoda bude vevnitř jen volat univerzální Handle. Ale ani tady se nemusíme zastavit – můžeme např. nagenerovat i metody se sufixem Sync, které zajistí, že Command bude okamžitě zpracován místo zařazení do fronty v MessageBusu (někdy se to může hodit).

Jak vidno, zadávání požadavků do Aplikace přes Commandy nám přináší neuvěřitelné možnosti a v tomto řešení vidím jediný problém – je těžké rozhodnout, který kód je součástí Aplikace (a měl by tedy být uvnitř Aplikace) a který kód je jen infrastrukturní (a měl by tedy být v Command Handleru). Rozhodně do Command Handleru patří konverze typů, základní validace a autorizace. Zbytek je na vašem rozhodnutí…

Queries

Dotazovací část fasády Aplikace zůstane zvenku prakticky stejná – pořád se bude jednat o synchronní volání metod, protože uživatel chce vidět produkty v gridu okamžitě. Navenek to tedy mohou zůstat klidně fasádní služby, které budou volat NĚCO vespod. Pokud jste čekali, že to NĚCO bude Aplikace, musím vás zklamat. Prezentovat uživateli data, která prošla skrze Doménovou vrstvu, to bývá jeden z největších problémů nejen z hlediska výkonu (načtete celou entitu a pak zobrazíte jen příjmení) tak z hlediska čistoty kódu – pokud jste např. přidali entitě Person readonly property DisplayName jen proto, že ji potřebujete v GUI, tak si rovnou dejte facku ;-)

Takže co bude to, co budou volat metody fasádních služeb (Queries) ? Nejefektivnější je sahat přímo do databáze – v případě .NET přes ADO.NET, příp. nějaký jednoduchý mapper (např. Dapper). Ať se vám to může při prvním pohledu zdát jakkoliv zvrhlé, po několika měsících musím uznat, že je to dobrá cesta a nevede k masivní duplikaci kódu, jak by to mohlo na první pohled vypadat.

Jak vidíme, de facto teď máme nad jednou persistentní vrstvou dvě aplikace – jedna se stará o zpracování Commandů a druhá o co nejrychlejší prezentaci dat uživateli (via Queries). Queries teď dělají to, že spouští (vysoce optimalizované) dotazy nad databází, které tahají jen ta data, která jsou opravdu potřeba.

Když budeme mít ale jen jednu databázi a aplikace zrovna bude pod velkým write-loadem (hodně Commandů), odnesou to Queries vyšší latencí – proto může být dobrý nápad rozdělit databázi na dvě:

OpDB (Operational Database) je běžná databáze v 3NF+, ke které Aplikace přistupuje běžným způsobem – např. pomocí full-featured ORM – v .NET se dá za něj považovat asi jen NHibernate, Entity Framework teprve bloumá okolo nádraží ;-)

K ReadDB přistupují Queries co nejpříměji, např. přes nějaký zmiňovaný lightweight-ORM. Účelem této databáze je umožnit servírovat data uživateli co nejrychleji. Nejrychlejší by bylo mít pro uživatele předrenderovanou celou stránku (HTML), jen ji přečíst na jeden request z databáze (přes Query, resp. ReadApp) a přes webový server poslat do prohlížeče. To je poněkud extrémní řešení, proto se spokojme s tím, že budeme schopni na jeden request (a řekněme do desítek ms) získat všechna data potřebná pro zobrazení celé stránky, tedy „ViewModel“ stránky. ViewModel můžeme v databázi mít uložen jako Json, XML, DTO serializované do BLOBu, jak je libo. Jediná podmínka by měla být, že jsme schopni získat všechna data na jeden jednoduchý dotaz (žádný join!).

Správně, ReadDB není nic jiného než předem napopulovaná cache, po které nechceme nic jiného než vytáhnout surová data na základě nějakého ID. Proto ReadDB nemusí být vůbec klasická relační databáze – může to být klidně NoSQL databáze! A naše aplikace pak bude cool! ;-)

Synchronizace

Je nabíledni, že musíme ReadDB průběžně upravovat podle toho, jak se mění OpDB (~synchronizovat). To se typicky děje (opět) pomocí Message Busu, kdy při úpravě Doménového modelu vyvoláme událost, že došlo k nějaké změně (ProductChanged). Kdokoliv (tedy včetně ReadApp) má pak šanci tuto událost odchytit a poupdatovat podle ní odpovídající předpřipravené ViewModely apod.

Pokud ale nechceme udělat takový velký skok v používaných technologiích, není nic ztraceno. Můžeme uvažovat o dvou databázích jen z konceptuálního hlediska, fyzicky budeme mít jen jednu klasickou relační databázi. Jak už jsem psal, na úplném začátku jsme v situaci, kdy v Queries píšeme dotazy nad OpDB. Abychom dosáhli rozumného výkonu, musíme i v tomto řešení začít duplikovat data – k tomu slouží v klasických relačních databázích indexy. Takže máme databázi s indexy, klasika.

Prvním krokem k možnému budoucímu přesunu na více fyzických databází je to, že vytvoříme pohledy (Views). Prakticky sice budeme dělat pořád to samé, ale dotazy už budou ve své finální podobě („SELECT * FROM ViewModel1 WHERE Id = @Id„) a nebudeme na ně tak muset v dalších krocích sahat.

Další iterací je to, že pohledy budou materializované. Pokud to náš DB stroj neumí, můžeme si je implementovat sami – jednoduše vytvoříme místo pohledů tabulky se stejnou strukturou. Pak si vytvoříme triggery nad původními tabulkami a podle zápisů do nich budeme upravovat naše materializované pohledy.
Je zřejmé, že se nám tím zpomalí zápis, ale čtení bude bleskurychlé – takže konkrétní řešení závisí i na tom, jaký je ve vaší aplikace poměr čtení/zápis (nejčastěji to prý bývá 80 % vs. 20 %).

Pokud budeme chtít ReadDB a OpDB skutečně fyzicky rozdělit (a pokud neumíme zajistit spouštění triggerů přes více DB strojů), musíme synchronizaci přenést o úroveň výše. A tím se dostáváme zpět k výše zmiňované synchronizaci přes události (Events), které můžeme vyvolávat tam, kde to bude pro naši aplikaci nejvhodnější – v DALu, v Doméně nebo třeba po úspěšném zpracování Commandu…

UI

Jak je vidět z posledního obrázku, naše GUI teď prakticky pracuje nad dvěma aplikacemi – jedna zajišťuje načítání dat a druhá zpracování dat. Načítání dat zůstává prakticky stejné jako v případě klasické (non-CQRS) aplikace, ale rozdíly ve zpracování dat jsou markantní. Už nejsme schopni garantovat to, že po odeslání objednávky ji zákazník hned uvidí v nějakém seznamu, protože třeba příkaz ještě není zpracován nebo ještě nestačila proběhnout synchronizace ReadDB. A tomu je třeba přizpůsobit koncepci uživatelského rozhraní, příp. si více pohrát s aplikací. V části o Commandech jsem se např. zmiňoval o možnosti trackování stavu Commandu…

V souvislosti s CQRS se často mluví o tzv. Task-Based UI, což je alternativa k běžnému CRUD UI. Není to něco, co je v CQRS povinné, ale považuji to za zajímavý koncept, proto ho chci letmo zmínit.
Klasické CRUD UI se vyznačuje tím, že umožňuje vytvořit entititu, upravit ji a příp. ji smazat. Problém je, že když voláme metodu Change, tak tím vůbec nevyjadřujeme záměr, proč tak činíme.
Bylo by mnohem lepší, kdychom měli připravené Commandy, které by vyjadřovaly, že např. měníme adresu zákazníka, protože se stěhuje (a podle nové adresy mu doporučit novou výchozí prodejnu), že měníme příjmení zákazníka, protože se oženil/vdala/registroval(a), že mažeme zákazníka, protože zemřel atd.
A pro tyto specifické tasky pak budeme mít specifické UI – Task-Based UI.
S problematikou ukládání takových informací do databáze souvisí Event-Sourcing, ale o tom třeba někdy jindy…

Co z toho?

CQRS je velmi zajímavý vzor, díky kterému jsem se dozvěděl o spoustě zajímavých konceptů, z nichž některé úspěšně používám v praxi. Ty nejzajímavější koncepty (MessageBus, ReportingDatabase) jsem se pokusil nastínit v tomto článku a věřím, že vás mohou inspirovat k dalšímu studiu CQRS a příp. i vylepšení vašich aplikací.

O CQRS není problém vygooglit spoustu materiálů (a možná se vás už ani Google nebude ptát „Did you mean CARS?“ ;-) ) – doporučuji především materiály od Grega Younga, Udiho Dahana a Rinata Abdullina.