Kapitola 21 · Praxe · Anti-vzory a typické chyby v DDD

Anti-vzory a typické chyby v DDD

Přehled nejčastějších anti-vzorů a typických chyb při implementaci Domain-Driven Design: anémický doménový model, Primitive Obsession, příliš velký agregát, sdílená databáze napříč Bounded Contexts, mutovatelné události a over-engineering.

Autor M. Katuščák
Doba čtení ≈ 35 min
Náročnost mírně pokročilá
Publikováno · Aktualizováno ·
Obsah kapitoly

21.01 Úvodem: Proč znát anti-vzory

Tato kapitola je katalog kódových a modelovacích anti-vzorů v DDD. Pro provozní/infrastrukturní třenice (Doctrine, Messenger, ACL k externím API, Symfony Form vs. Command) viz DDD v praxi – kde to bolí. Pro rozhodovací rámec, jestli DDD vůbec použít, viz Kdy DDD nepoužívat.

DDD nabízí strukturu pro modelování domény, ale s tou strukturou přicházejí specifická úskalí. Týmy začínající s DDD opakovaně narážejí na stejné chyby, i když teorii rozumějí. Anti-vzory je proto potřeba znát stejně dobře jako vzory samotné. Definice termínů použitých v této kapitole (entita, hodnotový objekt, agregát, bounded context) najdete v kapitole Základní koncepty DDD.

Anti-vzor je přístup, ke kterému vývojáři přirozeně sklouznou. Vypadá správně, ale narušuje principy DDD a dlouhodobě podkopává udržovatelnost, testovatelnost i výkon.

21.02 Anti-vzor: Anémický doménový model (Anemic Domain Model)

Anémický model je nejrozšířenější anti-vzor objektově orientovaného vývoje a v DDD zvlášť bolí. Termín popularizoval Martin Fowler v článku z roku 2003 [1]. Doménové třídy (entity, agregáty) v něm slouží pouze jako datové kontejnery. Obsahují výhradně gettery a settery a veškerá doménová logika je přesunuta do servisní vrstvy.

FIG. 22.2-A Anémický vs. bohatý doménový model – kde sedí logika

Rozdíl je v tom, že správná entita vystavuje doménově orientované metody (activate(), deactivate(), register()) místo generických setterů. Entita sama garantuje své invarianty – nikdo zvenčí ji nedostane do nekonzistentního stavu.

21.03 Anti-vzor: Primitive Obsession (posedlost primitivy)

Primitive Obsession nastává, když vývojáři používají primitivní datové typy (string, int, float) tam, kam patří hodnotové objekty (Value Objects). Primitiva působí na první pohled přímočaře, ale vedou k závažným problémům.

21.04 Anti-vzor: Příliš velký agregát (God Aggregate)

Agregát navrhujeme kolem transakční konzistence – tedy kolem nejmenší skupiny objektů, kterou je třeba měnit společně v jedné transakci. Příliš velký agregát (tzv. „God Aggregate“) sdružuje pod jeden kořen entity a logiku, které k sobě transakčně nepatří. Tím porušuje princip jedné odpovědnosti a způsobuje problémy popsané níže.

FIG. 22.4-A God Aggregate vs. správně rozdělené agregáty propojené přes ID

Pravidlo pro navrhování agregátů zní: agregát by měl být co nejmenší, aby zachoval invarianty (doménová pravidla) platné v jedné transakci. Pokud změna jednoho objektu nevyžaduje konzistentní změnu druhého ve stejné transakci, patří do různých agregátů.

21.05 Anti-vzor: Sdílená databáze napříč Bounded Contexts

Sdílená databáze napříč Bounded Contexts patří mezi nejzávažnější strategické anti-vzory. Nastává, když různé kontexty sdílejí stejné databázové tabulky nebo přistupují přímo k datům jiného kontextu. Na počátku to vypadá pragmaticky, ale vede k těsnému provázání, které blokuje nezávislý vývoj a nasazení jednotlivých kontextů.

Alternativou k synchronnímu HTTP volání je asynchronní komunikace přes doménové události. Billing kontext může naslouchat události CustomerBillingDataUpdated a lokálně si ukládat kopii potřebných dat (tzv. Read Model projection). Tím odstraníme synchronní závislost za cenu eventuální konzistence.

21.06 Anti-vzor: Mutovatelné doménové události

Doménová událost popisuje fakt, který se v minulosti stal. Minulost nelze měnit, a tak musí být událost striktně immutable (neměnná). Mutovatelná událost je konceptuální rozpor: pokud lze událost po vytvoření změnit, ztrácí svou sémantickou hodnotu jako historický záznam.

Mutovatelné události navíc způsobují praktické problémy při event sourcingu, auditních logách a při komunikaci mezi Bounded Contexts. Přijímající kontext totiž předpokládá, že obdrží konzistentní a neměnná data.

21.07 Anti-vzor: Doménová logika v infrastrukturní vrstvě

DDD striktně odděluje doménovou vrstvu od infrastrukturní. Infrastrukturní vrstva (Doctrine repozitáře, Symfony Forms, kontrolery, event listenery) by měla být tenká a delegovat veškerou doménovou logiku do doménové vrstvy. Doménová pravidla v infrastrukturních třídách narušují hranice vrstev a vytvářejí skrytou, těžko testovatelnou logiku.

21.08 Anti-vzor: Over-engineering u jednoduchých aplikací

DDD není vhodné pro každý projekt. Eric Evans upozorňuje, že největší přínos má u komplexních domén se složitou doménovou logikou. Pro CRUD aplikace, administrativní nástroje nebo prototypy je plnohodnotné DDD překombinované: přináší vysokou počáteční složitost bez odpovídajícího efektu.

21.09 Anti-vzor: Ignorování Ubiquitous Language

Ubiquitous Language je společný jazyk vývojářů, doménových expertů a dalších zainteresovaných stran. Používá se konzistentně v kódu, dokumentaci, testech i v komunikaci. Když tento princip selhává, tatáž doménová entita nese různé názvy na různých místech. Výsledkem jsou nedorozumění, chyby a ztráta doménového vhledu v kódu.

Znalost těchto anti-vzorů pomáhá udržet kvalitu doménového modelu po celý životní cyklus projektu. Vaughn Vernonova kniha Implementing Domain-Driven Design se anti-vzorům věnuje podrobně na praktických příkladech – viz doporučené zdroje.

Časté otázky

Co je anémický doménový model a jak ho poznat?

Anémický model vypadá na první pohled jako DDD – obsahuje třídy s názvy agregátů, entit a hodnotových objektů. Veškerá logika je ale přesunutá do služeb. Typickým znakem jsou gettery a settery jako jediné metody a třídy bez jakéhokoli pravidla uvnitř. Doménová logika končí ve „Service“ třídách, které manipulují s daty zvenku. Výsledkem je procedurální kód balený do objektových fasád. Detailní rozbor v sekci Anémický doménový model.

Proč je Primitive Obsession problém?

Primitive Obsession znamená používání primitivních typů (string, int, float) tam, kde patří doménový pojem. Místo typu Email se předává string, místo Money dvojice float. Důsledkem je, že validace a pravidla se opakují v každém místě volání, nebo se zapomínají. Hodnotový objekt s jedním místem validace tyto duplicity odstraňuje a typ dává kontext, co daná hodnota reprezentuje. Rozbor a příklady v sekci Primitive Obsession.

Jak poznat, že je agregát příliš velký?

Typické příznaky God Aggregate jsou tři. Agregát obsahuje desítky vnitřních entit. Jeho načtení zabere stovky SQL dotazů. Nebo souběžné operace nad různými částmi narážejí na optimistické zamykání. Pokud dvě metody agregátu řeší vzájemně nezávislá pravidla a nesdílejí invariant, pravděpodobně jde o dva samostatné agregáty. Hranice agregátu má kopírovat hranice transakční konzistence – nic víc. Praktický příklad refaktoringu v sekci Příliš velký agregát.

Proč je sdílená databáze mezi Bounded Contexts problém?

Sdílená databáze formálně drží data pohromadě, ale fakticky ruší hranice mezi Bounded Contexts. Změna schématu v jednom kontextu může rozbít druhý, pojmy se mísí a model jednoho týmu začíná záviset na modelu druhého. Správné řešení je, aby každý Bounded Context vlastnil svá data a komunikace probíhala přes definované rozhraní (API, události), nikoli přes sdílenou tabulku. Podrobný rozbor v sekci Sdílená databáze napříč Bounded Contexts.

Musí být doménová událost neměnná?

Ano. Doménová událost popisuje něco, co se již stalo – OrderPlaced, PaymentReceived – a minulost nelze měnit. Událost bez setterů, s neměnnými atributy a časovým razítkem vytvořeným při konstrukci je bezpečné sdílet mezi handlery, persistovat v event store a použít pro zpětnou rekonstrukci stavu. Mutovatelná událost vede k race condition, nedeterministickému zpracování a nekonzistentnímu auditu. Viz sekci Mutovatelné doménové události.