Mediator a CQS pro REST API
Tento článek byl napsán v roce 2021. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.
Na březen jsem připravil nový webinář s názvem „Mediator a CQS pro REST API“. Již několik měsíců používám na řadě projektů NuGet balíček MediatR, který mi výrazně pomáhá stavět RESTová API. Poznal jsem mnoho týmů, který tento přístup též používají a přišlo mi zajímavé téma pojmout jako webinář a uvést v souvislost několik různých principů a návrhových vzorů.
V čem je to tak skvělé?
Vývojáři se často ptají, jaká jedna funkcionalita je na MediatR tak skvělá. Inu, žádná. MediatR za vás nevyžehlí ani nevypere. Pomůže Vám ale dělat řadu drobných a užitečných věcí a nabídne Vám obrovskou flexibilitu při vývoji REST API. I proto většina konzultačních firem preferuje MediatR ve scénářích, kde není jasné, jaká bude cílová podoba architektury. Například když chybí kompletní požadavky na podobu REST API.
REST API využívají různé HTTP metody k provádění operací nad resources. Technicky se HTTP metody dělí na safe a non-safe. Safe metody jsou většinou pro čtení, druhé zmíněné pro zápis. A čtení a zápis jsou dva zcela odlišné světy. Svět queries a svět commandů. Když je něco odlišné, je dobré k tomu přistupovat různým způsobem. V tomto konkrétním případě je řeč o Command Query Separation principu (CQS).
Queries jsou dotazy na data. Ve světě RESTových API to jsou často data ve formátu JSON. Data chceme číst z REST API co nejrychleji. Často nám v tom pomáhá cache. Koneckonců HTTP protokol o safe metodách říká, že jsou cacheable a pokud chcete být RESTful, pak byste cachovat měli. Kromě toho JSON je typický formát pro dokumentové databáze. Co když tam už data čekají na vyžádání? Data se v každém případě přelévají pouze z jednoho místa na druhé. Často nám za tímto účelem pomáhá třeba Automapper.
Commands jsou příkazy. Změny v systému. Ve světě HTTP se bavíme o operacích POST, PUT, PATCH nebo DELETE. Změna dat je jiná. Nic se necachuje, ale zato se musí poctivě validovat. Uložení dat nebo změn do cílového úložiště je složitější proces, který je často doplněn o doménovou logiku. Tady se hodí objektově relační mapování. Můžeme načíst entitu a s pomocí change trackingu ji měnit, abychom ji následně nechali ORM persistovat zpět do úložiště. Zmíněný automapper z předešlého odstavce by tu ideálně neměl být, protože v rámci zápisu do úložiště nechceme automaticky propagovat nevalidované vstupy do dalších vrstev.
Queries a commands se spolu částo potkávají v action metodě controlleru. Například pro vytváření objednávky máme vstupní kontrakt, který předáme do commandu. Ten vytvoří objednávku a buď nevrátí nic (async api) nebo vrátí ID vytvořené objednávky. Na základě ID může z controlleru přes MediatR protéct query s žádostí o objednávku s daným ID. To je celý princip separace.
Mediator
Mediator je behaviorální design pattern. Máme třídu, která vystavuje určitou metodu a do ní předáme instanci třídy. Mediator se na základě předané instance třídy rozhodne, co provede. V praxi mám tedy metodu Konej(), do které naleju plyn a ona škrtne zapalovačem. Na první pohled žádný velký zázrak, protože téhož by bylo možné docílit i ošklivějším kódem. O tom ale design patterny jsou. Umožňují dělat ošklivé věci pěkně, snížit redundanci a občas i čitelnost kódu. Tedy pro méně zkušené vývojáře.
Jestliže může něco na základě vstupu měnit chování, proč to nepoužít k realizaci CQS? Proč nerozdělit queries a commands tak, že ke každému druhu požadavku bude Mediator přistupovat odlišně? To není žádný problém. Dříve či později ale zjistíme, že i k některým commandům chceme přistupovat trochu odlišně. A taky budeme například potřebovat, aby se na základě provedeného commandu dozvěděl query o tom, že má třeba invalidovat cache.
MediatR
Vývojář by neměl zbytečně psát něco, co už bylo vymyšleno. Chceme-li použít Mediator pattern pro účely handlování requestů v REST API, máme k dispozici hotový balíček MediatR. Ten už si prošel dlouholetým vývojem a obsahuje všechny myslitelné funkcionality, které uspokojí všechny možné potřeby komunikace. Navíc za tu dlouhou dobu pro MediatR vznikla řada rozšíření v podobě NuGet balíčků.
V praxi máme tedy hotovou infrastrukturu, kterou lze během pár minut zapojit do projektu. Komunikaci proti REST API můžeme rozdělit na commands a queries (všechno je v základu request) a pro každý command a query napsat tzv. handler. Protože dodržujeme pravidlo jeden handler pro jeden use case, ctíme i Single Responsibility Principle. Handler má minimum závislostí a je lehce testovatelný. Díky flexibilitě ale můžeme toto pravidlo potlačit a udělat handler, který bude obhospodařovat třeba dva commandy, které mají určitou společnou logiku.
Všechny handlery mohou mít společnou pipeline - tzv. behaviours. Snadno tedy lze přidat logiku, která bude počítat dobu běhu, logovat request response nebo provádět generickou validaci. Jenomže také můžeme naše queries a commandy odekorovat a udělat společnou behaviour pipeline jen pro určitou skupinu queries či commandů. No a nebo uděláme takovou pipeline jen pro jeden vybraný handler. A nebo to bude společný behaviour, který se bude provádět na základě aktuální konfigurace projektu. Flexibilita.
MediatR má navíc vestavěnou i podporu pro třetí druh artefaktu. Jsou jím notifikace. Každá notifikace může mít hned několik handlerů. Takže když odstraníme položku z databáze, můžeme použít notification handler, který zajistí aktualizaci cache. Nebo když dorazí na API nový command pro vytvoření účtu, vytvoříme notifikaci, kterou jeden handler obhospodaří a pošle uživateli uvítací e-mail, zatímco druhý handler odešle na jiné API požadavek na vystavení členské karty.
Repository
Jak to funguje společně s repository? MediatR nás v základu díky handlerům donutí k extrakci logiky mimo controllery. Tam stejně žádnou logiku nechceme. Cyklomatická komplexita metody controlleru by měla být 0. V anemickém modelu je výskyt repository spojen buď s logikou v controlleru, nebo s existencí dalších tříd obsahujících logiku (obvykle Facades nebo Services). Máte-li logiku v controlleru, přínos je zjevný. V ostatních případech si položte otázku, zda současný přístup (Facade, Service) mimo extrakci logiky z prezentační vrstvy něco přináší. Pakliže nic… není to škoda?
Přestože je možné MediatR a Repository pattern spolu používat, není to nezbytně nutné. Speciálně u CRUDových API, která používají moderní ORM (Entity Framework Core) jsou repositories většinou zbytečné a spíše překáží. Zkušený tým může efektivně používat přímo EF Core (který sám o sobě implementuje unit of work + repository). Modelujete-li aplikaci například ve stylu DDD nebo chcete zajistit opravdu smysluplnou abstrakci či persistence ignorance, pak samozřejmě proti Repository nelze nic namítat.
Další drobné výhody
Při práci s MediatR se mi líbí, že mohu mít funkční controller včetně připravených kontraktů, queries a commandů a handler nemusí ještě existovat. Lze si tak navrhnout podobu API bez nutnosti programovat logiku za ním. Vše lze samozřejmě zkompilovat. Vývojáři si tak celkem snadno mohou rozdělit práci.
Díky extrakci logiky do handleru je snadné mít více handlerů (tedy implementací) a dle potřeby mezi nimi přepínat. Oproti logice v controlleru nemusím složitě zakomentovávat kód nebo dělat jiné nesmyslnosti. Podle konfigurace projektu tak mohu handlery různě přepínat.
Při vývoji handlery mají out-of-the box vestavěnou podporu pro asynchronní volání, včetně předání cancellation tokenů. Mohu si tedy vybrat, zda bude mít handler synchronní nebo asynchronní implementaci. To vše při zachování jednoduchosti. Stále platí pravidlo jeden handler pro jeden use case. Testování je tak mnohem příjemnější. Díky minimu závislostí se navíc mnohem snadněji mockuje.
Z výše uvedeného vyplývá, že aplikace napsaná s MediatR je udržitelnější pro následný refactoring a například pro aplikaci CQRS patternu. Jestliže už odděluji query a command část aplikace, je extrakce velmi jednoduchá.
Samotná existence queries a commands přímo vybízí k zapouzdření. Do API tak deserializuji na kontraktní třídy, ale na úrovni handlerů mám queries a commands, které mohou mít například vlastní logiku nebo validace. Podobu queries a commandů mohu měnit bez vlivu na to, jak vypadá API navenek.
Vedle jednoduchosti handlerů mám k dispozici obrovskou flexibilitu. Každý handler je sice samostatná jednotka, ale snadno jej mohu odekorovat o další chování. To v klasickém „cihlovém“ pojetí anemického modelu obvykle chybí. Zejména když si tým vyrobí generický repository, generickou service, generický mapper a generický controller. Jakékoliv vybočení mimo triviální CRUD scénář je krokem do pekla.
Mám-li handler pro účely queries a nepoužívám repository, mohu v jednom handleru použít EF Core, v druhém ADO.NET a ve třetím třeba Dapper. Nejsem vázaný na generické repositáře. Ale zároveň mi nic nebrání v celém projektu si třeba jen jeden repositář udělat, protože mi to dává pro daný scénář smysl.
Síla MediatR je i v kombinaci s DDD. Na úrovni aggregate roots mohu sbírat události, které při Commitu v UnitOfWorku nasypu do MediatR. Ten pak rozposílá notifikace do notification handlerů a ty si s tím mohou dělat co potřebují. Třeba pošlou zprávy do message busu.
Závěr a pozvánka na webinář
Výhod má MediatR plno, jen jsou menší a tak jeho přítomnost v projektu vývojář ocení až během procesu vývoje. Implementace balíčku je snadná a jeho použití v zásadě také. Připravil jsem webinář, ve kterém se zaměřím přímo na návrhový vzor Mediator a ukážu jeho přínos na praktickém příkladu. Dokážete jej tedy využít v libovolné aplikaci a zároveň pochopíte princip fungování balíčku MediatR. Ten je však sám o sobě mocnější, takže se na něj podíváme podrobněji právě v souvislosti se CQS principem.