Reference · Glosář DDD terminologie

Glosář DDD terminologie

Akademický slovník hlavních pojmů Domain-Driven Design – od strategických vzorů přes taktické stavební bloky až po architektonické a integrační vzory. Definice vycházejí z primární literatury (Evans 2003, Vernon 2013).

Autor M. Katuščák
Publikováno · Aktualizováno ·
Obsah kapitoly

01 Strategické vzory DDD

Co jsou strategické vzory?

Strategické vzory dělí systém na soudržné celky a pojmenovávají vztahy mezi nimi. Odpovídají na otázku „co a kde“: kde leží hranice modelu, kdo za kterou část domény odpovídá a jak spolu části komunikují. Taktické stavební bloky stojí až na tomto základu. Bez něj z nich zbude náhodná sbírka vzorů.

Doména (Domain)

Sféra znalostí a činností, kolem níž se točí logika aplikace. Doména je „proč“ softwaru – problémový prostor, který software modeluje a pomáhá řešit. Eric Evans definuje doménu jako: „A sphere of knowledge, influence, or activity. The subject area to which the user applies a program.“ Doména není totéž co aplikace samotná – existuje i bez softwaru a software je jen jeden z prostředků, jak v ní pracovat. Kdo doméně nerozumí, nenavrhne pro ni smysluplný model.

Zdroj: Eric Evans, Domain-Driven Design (2003)

Subdoména (Subdomain)

Část celkové domény vymezená v problémovém prostoru. Subdomény vznikají analýzou domény, která problémový prostor rozdělí na soudržné oblasti. Kniha rozlišuje tři typy: Core Domain přináší konkurenční výhodu, Supporting Subdomain je nutná pro provoz, ale výhodu nepřináší, a Generic Subdomain pokryje hotový produkt. Subdomény patří do problémového prostoru, Bounded Contexty jsou jejich protějškem v prostoru řešení. Jeden Bounded Context obvykle implementuje jednu subdoménu, poměr 1:1 ale není pravidlo.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 15; Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 2

Core Domain (jádrová doména)

Subdoména, která přináší největší konkurenční výhodu. Organizace se v ní odlišuje od konkurence a leží v ní hlavní důvod, proč software vzniká. Dostává největší investice, nejzkušenější vývojáře a plnou sadu taktických vzorů DDD. Ostatní subdomény ji podpírají, samy cílem nejsou. Určit Core Domain patří k hlavním výstupům strategické analýzy.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 15; Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 2

Supporting Subdomain (podpůrná subdoména)

Subdoména, bez které Core Domain nefunguje, ale konkurenční výhodu sama nepřináší. Příklad: sledování objednávek v e-shopu, který se od konkurence odlišuje doporučovacím systémem. Supporting subdomény se obvykle vyvíjejí vlastními silami, ovšem bez plné sady taktických vzorů, protože jejich složitost to nevyžaduje. Jsou nezbytné, ne strategické. Kde je jde nahradit hotovým řešením, bývá to levnější cesta.

Zdroj: Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 2

Generic Subdomain (generická subdoména)

Subdoména, jejíž potřeby pokryje hotové řešení (off-the-shelf software). Příklady: autentizace a správa identit (Keycloak, Auth0), platby (Stripe), odesílání e-mailů (Mailgun, SendGrid). Nic z toho není specifické pro konkrétní podnikání, stejný problém řeší tisíce organizací. Vlastní vývoj proto obvykle plýtvá kapacitou. Rozumná strategie je koupit, pronajmout nebo převzít hotový produkt. Autorizační pravidla nad vlastními entitami ale zůstávají v kontextu, který je používá.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 15; Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 2

Ubiquitous Language (všudypřítomný jazyk)

Sdílený a přesný jazyk vývojářů a doménových expertů. Tým ho důsledně používá v kódu, v rozhovorech, v dokumentaci, v testech i v diagramech. Jeho slova jsou pojmy z domény, ne technické termíny ani databázový slang. Platnost jazyka končí na hranici Bounded Contextu: stejné slovo může v jiném kontextu znamenat něco jiného (například „zákazník“ v prodeji a v podpoře). Evans ho staví do základu celého přístupu. Bez sdíleného jazyka zůstane z DDD sada technických vzorů.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 2

Bounded Context (ohraničený kontext)

Explicitní hranice, uvnitř níž platí jeden doménový model a jeho Ubiquitous Language. Každý pojem v ní má jeden přesný význam. Jiný Bounded Context může totéž slovo použít pro odlišný koncept, aniž by si modely odporovaly. Jde o logickou hranici modelu a jazyka. V kódu ji obvykle promítá jeden modul (v Symfony namespace jako App\Ordering). Provozně může kontext běžet v modulárním monolitu i jako samostatná služba. Mapování na týmy, databázová schémata a nasazení je samostatné rozhodnutí, rovnice „Bounded Context = microservice“ je zkratka, ne definice.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14; Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 2

Context Map (kontextová mapa)

Dokument nebo diagram, který ukazuje všechny Bounded Contexty v systému a vztahy mezi nimi. Nejde primárně o technický artefakt. Context Map zachycuje vztahy týmů, směr závislostí (kdo závisí na kom), integrační kontrakty a použité vzory (Shared Kernel, Customer/Supplier, Anti-Corruption Layer a další). Bývá prvním hmatatelným výstupem strategické analýzy. Kreslí se skutečný stav, ne ten, který si tým přeje.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Partnership (partnerství)

Symetrický vztah dvou Bounded Contextů, jejichž týmy společně uspějí, nebo společně padnou. Modely zůstávají oddělené, plánování ale ne: týmy koordinují vývoj, sladí release a integraci řídí společně. Takový vztah obvykle vznikne sám a Context Map mu jen dá jméno a proces. Evans k němu přidává testovací sadu, která v continuous integration ověřuje, že rozhraní odpovídá očekáváním druhé strany.

Zdroj: Eric Evans, Domain-Driven Design Reference (2015)

Shared Kernel (sdílené jádro)

Symetrický vztah, v němž dva nebo více Bounded Contextů sdílejí výslovně vymezenou podmnožinu doménového modelu včetně jejího kódu. Evans obsah kernelu neomezuje. V praxi se ale drží malý, typicky jen elementární hodnotové objekty (Money, Currency). Každou změnu musí odsouhlasit všichni vlastníci a projít testy všech kontextů. Těsná vazba má svou cenu, proto se kernel vyplatí jen tam, kde přínos sdílení převáží náklady koordinace. Sdílená technická knihovna (logger, HTTP klient) Shared Kernel není. Namespace App\SharedKernel v ukázkách knihy nese i technické bázové typy (rozhraní busů, abstraktní třídy). Shared Kernel v Evansově smyslu tvoří jen jeho doménová část.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Customer/Supplier (zákazník/dodavatel)

Asymetrický vztah, v němž downstream kontext (customer) závisí na upstream kontextu (supplier). Na rozdíl od Conformistu má downstream hlas: jeho požadavky se vyjednávají, rozpočtují a dostávají se do backlogu suppliera. O tom, kdy a jak je supplier dodá, ale rozhoduje on. Vzor předpokládá, že spolu týmy pravidelně plánují. Když supplier potřeby customera přestane brát v úvahu, vztah sklouzne ke Conformistu, nebo si downstream postaví Anti-Corruption Layer.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Conformist (konformista)

Role downstreamu v asymetrickém vztahu: downstream vědomě rezignuje na vlastní model a přebírá upstream model 1:1, bez překladu. Volí se tam, kde upstream tým nemá důvod vycházet downstreamu vstříc a jeho model je pro downstream dost dobrý. Conformist ušetří překladovou vrstvu, platí za to závislostí na cizím modelu. Opačnou volbou downstreamu je Anti-Corruption Layer. Conformist sedí reportingovému nebo průchozímu kontextu, který data jen zobrazuje či přeposílá, a oborovým nebo regulatorním formátům. Jakmile na převzatých datech stojí vlastní doménová pravidla, patří na hranici ACL. Proto kniha platby (Stripe) i identitu (Auth0) napojuje přes ACL.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Anti-Corruption Layer (ACL)

Role downstreamu, který se před cizím modelem brání izolační vrstvou. ACL odděluje Bounded Context od modelů jiných kontextů, legacy systémů nebo externích API. Pojmy cizího modelu překládá do vlastního Ubiquitous Language, validuje vstupy a neplatné stavy odfiltruje dřív, než dorazí do domény. Tím brání tomu, aby cizí koncepty „zkazily“ doménový model. Implementuje se jako sada adaptérů, fasád a překladačů na hranici kontextu. Ze všech rolí se používá nejčastěji.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Open Host Service (OHS)

Role upstreamu, který otevřeně publikuje stabilní, dokumentovaný protokol pro mnoho downstream konzumentů. Místo samostatné integrační dohody s každým konzumentem udržuje jeden veřejný protokol a downstreamy se mu přizpůsobí. K internímu modelu upstreamu přitom přístup nepotřebují. Vyplatí se od zhruba tří konzumentů a u stabilní domény. Potřeby jediného týmu řeší jednorázový překladač, aby sdílený protokol zůstal malý a soudržný.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Published Language (PL)

Dobře dokumentovaný, formálně specifikovaný formát zpráv mezi Bounded Contexty, nezávislý na jazyce, frameworku i databázi. Často jde o oborový standard (HL7 ve zdravotnictví, FIX ve financích, ISO 20022 v bankovnictví) nebo o vlastní JSON Schema či OpenAPI. Výměnným formátem nemá být přímo doménový model jednoho z kontextů. Každá změna modelu by pak měnila veřejný kontrakt. Published Language se obvykle kombinuje s Open Host Service: OHS je kanál, PL formát.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Separate Ways

Strategické rozhodnutí, že dva Bounded Contexty nebudou integrované vůbec. Každý tým řeší problém sám a přijímá duplicitu dat nebo paralelní procesy. Hodí se tam, kde by integrace stála víc, než přinese: sdílená funkce není pro žádný kontext kritická nebo komunikace mezi týmy vázne. Separate Ways je vědomá volba, ne selhání návrhu. Duplicita tu vychází levněji než koordinace.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 14

Big Ball of Mud (anti-vzor)

Stav rozpadu, kdy každý kontext „nějak“ mluví s každým a sdílí s ním databázové tabulky, entity i ad hoc servisní vrstvy. Hranice modelů nikdo neumí ukázat, vztahy nejsou pojmenované a Context Map proto nejde nakreslit. Pojem zavedli Brian Foote a Joseph Yoder (1997). Evans ho v DDD Reference uvádí vedle osmi vzorů Context Mappingu jako devátý. Na rozdíl od nich si ho nikdo vědomě nevolí: vznikne, když tým žádnou volbu neudělá.

Zdroj: Brian Foote & Joseph Yoder, Big Ball of Mud (1997); Eric Evans, Domain-Driven Design Reference (2015)

02 Taktické vzory (stavební bloky DDD)

Co jsou taktické vzory?

Taktické vzory jsou objektové stavební bloky, kterými se doménový model implementuje uvnitř jednoho Bounded Contextu. Odpovídají na otázku „jak“: jak modelovat entitu, vynucovat invarianty, uchovávat stav a vyjádřit doménové operace. Smysl mají jen uvnitř strategického rámce, který určí, kde model platí.

Entita (Entity)

Doménový objekt s jedinečnou, přetrvávající identitou, která ho odlišuje, i když se změní všechny jeho atributy. Příklad: uživatel identifikovaný hodnotou UserId zůstává stejnou entitou, přestože změní jméno, e-mail i adresu. Dvě entity se stejnými daty, ale odlišnými identifikátory, jsou různé entity. Rovnost entit určuje identita, ne stav. Na rozdíl od hodnotového objektu potřebuje entita vlastní identifikátor. Kniha ho modeluje jako silně typovaný hodnotový objekt generovaný přes Uuid::v7().

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 5

Hodnotový objekt (Value Object)

Doménový objekt bez identity: dva hodnotové objekty se stejnými atributy jsou rovnocenné a zaměnitelné. Jsou neměnné (immutable), takže každá „změna“ vrací novou instanci. Příklady: Money, Email, Address, DateRange, ProductId. Kniha je píše s veřejnými readonly vlastnostmi ($email->value). Před primitivním typem mají přednost: vyjadřují doménový záměr, drží validaci na jednom místě a brání záměně argumentů. Mnoho konceptů, které vývojáři ukládají jako řetězec nebo číslo, je ve skutečnosti hodnotový objekt.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 5

Agregát (Aggregate)

Shluk doménových objektů (entit a hodnotových objektů), se kterým se při změně dat zachází jako s jednou jednotkou. Má právě jeden kořen agregátu a vynucuje invarianty uvnitř své hranice: žádná operace ho nesmí uvést do neplatného stavu. Jiné agregáty na něj odkazují jen identifikátorem kořene, nikdy přímou referencí na vnitřní entity. Hranice agregátu je zároveň hranicí transakce. Výchozí vodítko zní: jedna transakce mění jeden agregát, výjimky rozebírá kapitola Návrh agregátu. Určit správnou velikost agregátu patří k nejtěžším úlohám DDD. Příliš velký agregát při každé operaci načítá desítky vnitřních entit, příliš malý nechá invarianty bez ochrany.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 6; Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 10

Kořen agregátu (Aggregate Root)

Jediná entita agregátu, přes kterou vede veškerý přístup zvenčí. Vynucuje všechny invarianty agregátu, a proto vnější kód mění vnitřní objekty jen jeho metodami. Jiné agregáty si pamatují jen identifikátor kořene, nikdy referenci na vnitřní entity. Kořen je také jediný objekt, který repozitář přímo ukládá a načítá. V knize dědí z AggregateRoot, který nahrává doménové události metodou record() a vydává je metodou releaseEvents().

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 6

Repozitář (Repository)

Abstrakce pro ukládání a načítání agregátů, která se chová jako kolekce. Detaily persistence před doménou skrývá: z jejího pohledu jde o kolekci objektů v paměti. Rozhraní repozitáře patří do doménové vrstvy, implementace (Doctrine, in-memory) do infrastruktury. Na jeden typ agregátu připadá jeden repozitář. Vrací celé agregáty, dotazy na vnitřní entity nenabízí. Čtení pro obrazovky a reporty obsluhují jiné mechanismy (projekce, read modely).

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 6

Továrna (Factory)

Zapouzdřuje složitou logiku vzniku agregátu nebo hodnotového objektu tam, kde konstruktor nestačí. Když je vznik objektu sám doménovým aktem, pojmenovaný konstruktor ho pojmenuje (Order::place()). Továrna může být statická metoda na agregátu, samostatná třída nebo doménová služba. Odděluje odpovědnost za vytvoření objektu od jeho použití. Odpovídá za to, že vzniklý objekt splňuje invarianty, logiku jeho pozdějšího chování ale nenese.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 6

Doménová služba (Domain Service)

Bezstavová služba s doménovou operací, která nepatří žádné konkrétní entitě ani hodnotovému objektu. Příklady: přepočet měn, výpočet ceny se slevami ze dvou různých agregátů, převod peněz mezi účty. Patří do doménové vrstvy a mluví Ubiquitous Language. S aplikační službou se často plete, rozdíl je v obsahu: aplikační služba orchestruje use case, doménová vyjadřuje doménové pravidlo.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 5

Aplikační služba (Application Service)

Orchestruje use case: načte agregáty z repozitářů, zavolá doménové metody, uloží výsledek a publikuje doménové události. Doménovou logiku neobsahuje, ta patří do entit, hodnotových objektů a doménových služeb. Tvoří tenkou vrstvu mezi prezentací a doménou. V CQRS jí odpovídají command handlery a query handlery, v Symfony třídy s #[AsMessageHandler]. V hexagonální architektuře ji volá driving adaptér přes driving port.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 4; Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 14

Doménová událost (Domain Event)

Neměnný záznam o něčem významném, co se v doméně stalo, pojmenovaný v minulém čase a bez přípony „Event“: UserRegistered, OrderPlaced, PaymentFailed. Název patří do Ubiquitous Language. Doménové události propojují agregáty uvnitř jednoho Bounded Contextu. Hranici kontextu překračuje až integrační událost, tedy samostatný veřejný kontrakt s primitivními daty. Kolik dat událost nese, je rozhodnutí. Uvnitř kontextu obvykle stačí identifikátory, zbytek si příjemce načte sám. Agregát události nahrává v doménových metodách a pojmenovaných konstruktorech, nikdy v __construct. V Event Sourcingu jsou doménové události primárním způsobem persistence stavu.

Zdroj: Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 8; Eric Evans, Domain-Driven Design Reference (2015)

Integrační událost (Integration Event)

Událost určená pro jiné Bounded Contexty. Na rozdíl od doménové události je veřejným kontraktem, a proto se mění jen tak rychle, jak to její příjemci snesou. Nese jen primitivní hodnoty, aby přežila serializaci, a vlastní eventId pro deduplikaci u příjemce. V knize je to samostatná třída v aplikační vrstvě, například OrderPlacedIntegrationEvent vedle doménové OrderPlaced. Handler ji sestaví a uloží do outboxu ve stejné transakci jako změnu agregátu. Poslat ven přímo doménovou událost znamená zveřejnit vnitřní model.

Zdroj: Cesar de la Torre, Domain events vs. Integration events in Domain-Driven Design and microservices architectures (Microsoft DevBlogs)

Specifikace (Specification)

Zapouzdřuje obchodní pravidlo do znovupoužitelného a skládatelného objektu: predikátu, který určuje, zda doménový objekt splňuje dané kritérium. Specifikace se skládají logickými operátory AND, OR a NOT, takže složitou podmínku jde poskládat z jednodušších. Pravidlo tím dostane jméno v Ubiquitous Language („zákazník je premium“). Vzor formálně popsali Evans a Fowler v článku Specifications (1997), Evans ho pak zařadil do knihy Domain-Driven Design (2003).

Původní článek uvádí tři použití. Validace ověřuje, že objekt pravidlo splňuje (má objednávka nárok na dopravu zdarma?). Selekce vybírá z kolekce objekty podle kritéria, například zákazníky pro věrnostní program. Konstrukce na zakázku popisuje, jak má nový objekt vypadat, aniž řeší, jak ho vyrobit. Specifikace drží na jednom místě pravidla, která by se jinak rozptýlila po doméně, a dají se testovat izolovaně.

Zdroj: Evans & Fowler, Specifications (1997); Eric Evans, Domain-Driven Design (2003), kap. 9

Invariant (Invariant)

Obchodní pravidlo, které musí uvnitř hranice agregátu platit vždy, ať se agregát mění jakkoli. Vynucuje ho kořen agregátu. Příklady: „Objednávka musí mít alespoň jednu položku, aby mohla být potvrzena.“ a „Celková cena objednávky nesmí být záporná.“ Invarianty definují, co je platný stav, a tvoří tak jádro doménového modelu. Pokus o jejich porušení končí výjimkou, nikdy tichým průchodem.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 6

Doménová výjimka (Domain Exception)

Vlastní třída výjimky, která pojmenuje porušené doménové pravidlo slovy Ubiquitous Language. Místo obecných výjimek (InvalidArgumentException, RuntimeException) doménový model vyhazuje výjimky podle pravidla: InvalidOrderStateTransitionException, DuplicateEmailException, EmptyOrderException. Kód se díky nim lépe čte a chyby se snáz diagnostikují. Volající navíc může na konkrétní selhání reagovat zvlášť, třeba jiným HTTP statusem. Když by operace porušila invariant, agregát ji odmítne právě doménovou výjimkou. Výjimku tvoří porušení formátu v konstruktoru hodnotového objektu (neplatný e-mail, záporná částka): to kniha hlásí jako \InvalidArgumentException. Holý \DomainException je v knize jen přiznaná zkratka.

Zdroj: Eric Evans, Domain-Driven Design (2003); Vaughn Vernon, Implementing Domain-Driven Design (2013)

03 Architektonické vzory

Co jsou architektonické vzory v kontextu DDD?

Architektonické vzory určují, jak se organizují vrstvy a komponenty systému, jak spolu komunikují a kde leží odpovědnosti. DDD žádnou architekturu nepředepisuje. Kombinuje se s vrstvenou i hexagonální architekturou stejně jako s vertikálními řezy. Na volbě závisí testovatelnost, udržovatelnost a to, jestli doménový model zůstane bez infrastrukturních závislostí.

Vrstvená architektura (Layered Architecture)

Klasická čtyřvrstvá architektura, jak ji popisuje Evans (2003): prezentační vrstva → aplikační vrstva → doménová vrstva → infrastrukturní vrstva. Závislosti vedou jedním směrem, vrstva smí záviset jen na vrstvách pod sebou. V Evansově původním modelu závisí doména na infrastruktuře. Pozdější varianty (hexagonální a Onion architektura) tuto závislost obracejí přes rozhraní, takže doména nezávisí na žádné technické vrstvě. Pro DDD je vrstvená architektura přirozený výchozí bod. Kód ale třídí podle technické role, ne podle byznysové funkce, a z adresářů proto není vidět, co systém dělá.

Zdroj: Eric Evans, Domain-Driven Design (2003), kap. 4

Hexagonální architektura (Hexagonal Architecture / Ports and Adapters)

Architektura, kterou navrhl Alistair Cockburn. Jádro aplikace (doména a aplikační vrstva) komunikuje s vnějším světem jen přes porty (rozhraní) a adaptéry (implementace). Driving adaptéry (HTTP controller, konzolový příkaz) volají aplikaci, driven adaptéry (databáze, messaging, e-mail) naopak volá aplikace. Hlavní přínos: doménový model je od infrastruktury izolovaný a jde ho testovat bez databáze, HTTP frameworku i externích systémů. Princip inverze závislostí (DIP) se tu uplatňuje systematicky.

Zdroj: Alistair Cockburn, Hexagonal Architecture (2005), alistair.cockburn.us; Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 4

Port (Port)

V hexagonální architektuře rozhraní, přes které aplikace komunikuje s vnějším světem. Driving porty určují, jak aplikaci volají vnější aktéři (například MessageBusInterface nebo vlastní rozhraní use casu). Driven porty popisují, co aplikace potřebuje od infrastruktury (například UserRepository, EmailSender). Porty patří do aplikačního jádra, tedy do doménové nebo aplikační vrstvy, nikdy do infrastruktury.

Zdroj: Alistair Cockburn, Hexagonal Architecture (2005)

Adaptér (Adapter)

V hexagonální architektuře konkrétní implementace portu. Adaptér překládá mezi protokolem vnějšího světa a rozhraním aplikačního jádra. Příklady: DoctrineUserRepository implementuje port UserRepository; SymfonyMailerEmailSender implementuje port EmailSender; HTTP controller je driving adaptér, který volá aplikaci přes driving port (například MessageBusInterface). Adaptéry patří do infrastrukturní vrstvy a jdou vyměnit, aniž by se změnil doménový model.

Zdroj: Alistair Cockburn, Hexagonal Architecture (2005)

Vertical Slice Architecture (vertikální řezy)

Organizuje kód podle funkce a use casu, ne podle technické vrstvy. Každý slice (například Registration) je svislý řez celým stackem: controller, command, handler, doménová logika a repozitář leží pohromadě. Funkce jsou na sobě méně závislé, takže změna jedné se ostatních nedotkne. Klasické horizontální vrstvení naopak drží všechny controllery na jednom místě a všechny repozitáře na druhém. Slice odpovídá jednomu use casu a slices se dál seskupují podle Bounded Contextů.

Zdroj: Jimmy Bogard, Vertical Slice Architecture (2018), jimmybogard.com

CQRS (Command Query Responsibility Segregation)

Architektonický vzor, který odděluje čtení (queries) od zápisu (commands). Každá strana má vlastní model. Write model stojí na bohatých agregátech a vynucuje invarianty, read model vrací optimalizovaná DTO nebo projekce. Oddělené úložiště je volitelné, součástí definice není. Popsal ho Greg Young. Kořeny sahají ke CQS Bertranda Meyera, Young ale formulaci „CQRS je rozšíření CQS“ sám označuje za nepřesnou a pojímá CQRS jako samostatný vzor. V DDD doplňuje doménový model, který je na přímé zobrazení v UI příliš bohatý. Každou stranu pak jde optimalizovat zvlášť.

Zdroj: Greg Young, CQRS Documents (2010); Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 4

CQS (Command-Query Separation)

Princip Bertranda Meyera: metoda je buď command (mění stav a vrací void), nebo query (vrací data bez vedlejších efektů), nikdy obojí. Kód, který CQS porušuje, se hůř testuje a při opakovaném volání se chová nepředvídatelně. CQRS z tohoto principu vychází, ale neaplikuje ho mechanicky: odděluje celé modely, ne jednotlivé metody. Podmínkou přitom CQS není, CQRS jde zavést i bez jeho důsledného dodržení na úrovni metod.

Zdroj: Bertrand Meyer, Object-Oriented Software Construction, 2. vydání (1997), kap. 23; princip poprvé v 1. vydání (1988)

Event Sourcing

Vzor persistence, který místo aktuálního stavu ukládá append-only sekvenci doménových událostí. Stav agregátu vznikne přehráním streamu událostí od začátku, případně od posledního snapshotu. Stream zároveň slouží jako auditní log: každá změna zůstane zaznamenaná. Stav jde zrekonstruovat k libovolnému okamžiku v minulosti. S CQRS se doplňují: write model generuje události a projektory z nich staví read modely.

Zdroj: Greg Young, CQRS Documents (2010); Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 4

Event Store (Event Store)

Append-only úložiště doménových událostí v Event Sourcingu. Události ukládá do streamu každého agregátu se zárukou pořadí. Záznamy se nikdy nemění ani nemažou, to je základní invariant Event Sourcingu. Event Store obvykle nabízí zápis nových událostí do streamu s kontrolou verze (optimistické zamykání), čtení streamu od libovolné pozice a odběr nových událostí. Příklady: EventStoreDB, Marten (PostgreSQL) nebo vlastní implementace nad relační databází.

Zdroj: Greg Young, CQRS Documents (2010)

Projekce (Projection)

Read model postavený ze streamu událostí. Projektor naslouchá doménovým událostem a aktualizuje denormalizovanou strukturu optimalizovanou pro čtení (tabulku, dokument, cache). Projekci jde kdykoli zahodit a znovu sestavit přehráním událostí z Event Store, protože jde o odvozená data, ne o zdroj pravdy. Jedna sada událostí může napájet několik projekcí s různou strukturou pro různé obrazovky.

Zdroj: Greg Young, CQRS Documents (2010); Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 4

Snapshot (snímek)

V Event Sourcingu průběžně ukládaný stav agregátu, který zkracuje přehrávání streamu. Místo všech událostí od začátku (může jich být tisíce) se načte poslední snapshot a přehrají se jen události po něm. Jde o optimalizaci výkonu, správnost modelu neovlivňuje. Zavádí se, až když replay měřitelně zpomalí. Snapshoty jsou volitelné a zdrojem pravdy nejsou.

Zdroj: Greg Young, CQRS Documents (2010)

Sága / Process Manager (Saga / Process Manager)

Sága rozkládá dlouhotrvající obchodní proces přes více agregátů nebo Bounded Contextů na sérii lokálních transakcí. Když některý krok trvale selže, kompenzační transakce vrátí dokončené kroky v opačném pořadí a systém se dostane do konzistentního stavu. Jde o sémantickou obdobu rollbacku v distribuovaném prostředí. Příklad: dokončení objednávky s platbou, rezervací skladu a odesláním notifikace. Termín „sága“ pochází z databázového výzkumu (Garcia-Molina a Salem, 1987). Process Manager není synonymum, ale orchestrační komponenta s perzistentním stavem, která ságu řídí. Popisují ho Enterprise Integration Patterns (Hohpe & Woolf, 2003).

Ságu jde koordinovat dvojím způsobem. Při choreografii žádný centrální koordinátor není: každý účastník reaguje na události ostatních a publikuje vlastní. Při orchestraci řídí průběh Process Manager, který přijímá události, rozhoduje o dalším kroku a vydává příkazy. Mezi nimi nerozhoduje počet kroků, ale tvar procesu. Lineární posloupnost známou předem zvládne choreografie, Process Manager se vyplatí tam, kde se další krok určuje až za běhu nebo kroky neběží za sebou. Terminologie se mezi autory liší, kapitola Ságy a Process Managery rozebírá, kterou konvenci kniha drží.

Zdroj: Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 4; Hohpe & Woolf, Enterprise Integration Patterns (2003); Garcia-Molina & Salem, Sagas (1987)

04 Integrační vzory a komunikace

Co jsou integrační vzory?

Pojmy v této sekci popisují, jak spolu části systému komunikují, jak se v distribuovaném prostředí udržuje konzistence a jak se předchází konfliktům při souběžném přístupu. Patří sem i dva anti-vzory, tedy běžné chyby v modelování domény. Kdo tyto pojmy nezná, navrhuje systém odolný vůči souběhu a výpadkům komponent jen stěží.

Eventual Consistency (výsledná konzistence)

Model konzistence, v němž se repliky dat sejdou na stejné hodnotě, pokud už nepřicházejí další změny. Nemusí to ale být okamžitě. V DDD s CQRS může read model po zpracování příkazu a publikování události chvíli zaostávat za write modelem. Takové zpoždění bývá přijatelné, byznys o něm ale musí vědět a počítat s tím, že čerstvost dat má meze. Mezi agregáty a v distribuovaných systémech (CAP teorém) jde o nutný kompromis.

Zdroj: Werner Vogels, Eventually Consistent (ACM Queue, 2008); Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 4

Optimistické zamykání (Optimistic Locking)

Řízení souběžnosti bez zámků, založené na čísle verze. Při zápisu se kontroluje, zda verze v databázi odpovídá verzi načtené při čtení. Když mezitím agregát změnil jiný proces, vznikne konflikt a operace selže. Klient pak načte nový stav a operaci zopakuje. V Event Sourcingu se verze odvozuje z pozice ve streamu. Hodí se tam, kde ke konfliktům dochází zřídka. Pozor na Doctrine: #[ORM\Version] na kořeni se zvedne jen při změně vlastností kořene, ne při změně potomka.

Zdroj: Martin Fowler, Patterns of Enterprise Application Architecture (2002)

Pesimistické zamykání (Pessimistic Locking)

Řízení souběžnosti, které zdroj opravdu zamkne v databázi (typicky SELECT … FOR UPDATE nebo LOCK TABLES) na celou dobu transakce. Souběžné operace nad stejným záznamem se zablokují a čekají ve frontě. Hodí se při častých konfliktech, krátkých transakcích a tam, kde opakování operace stojí víc než čekání. Cenou je nižší propustnost a riziko uváznutí (deadlocku), které se snižuje jednotným pořadím zamykání a krátkým držením zámků. V Event Sourcingu se obvykle nepoužívá, protože události ve streamu jsou seřazené a kontrola verze stačí.

Zdroj: Martin Fowler, Patterns of Enterprise Application Architecture (2002); Bernstein & Newcomer, Principles of Transaction Processing (2009), kap. 6

Read model (čtecí model)

Datová struktura optimalizovaná pro čtení, oddělená od write modelu (doménového modelu). Pojem pochází z CQRS. Write strana drží invarianty v agregátech a pracuje přes repozitáře, read strana plní obrazovky a reporty předpočítanými, denormalizovanými daty. Read model jde udržovat synchronně v téže transakci jako zápis, nebo asynchronně s krátkým zpožděním. V obou případech ho obvykle plní projekce, které naslouchají doménovým událostem. Často spojuje data více agregátů, jejichž společný dotaz by na write straně narušil jejich izolaci. Každý use case může mít vlastní read model a jeden agregát tak přispívá do několika z nich.

Zdroj: Greg Young, CQRS Documents (2010); Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 4 a 8

Kompenzace (Compensation / Compensating Action)

Operace, která sémanticky vrací efekt dříve provedeného kroku ságy bez ACID rollbacku. Příklad: RefundCustomer kompenzuje ChargeCustomer, ReleaseStock kompenzuje ReserveStock. Kompenzační krok musí být idempotentní (opakované volání má stejný efekt jako jediné) a tolerantní k absenci kompenzovaného kroku (například když proces selhal ještě před rezervací). Kompenzace se spouštějí v opačném pořadí dokončených kroků. Komutativní operace jsou jiný nástroj: Richardson je uvádí jako protiopatření proti izolačním anomáliím mezi souběžnými ságami. Některé efekty vrátit nejde (odeslaný e-mail, fyzický výdej zboží). Kompenzace pak vypadá jinak: omluvný e-mail, vrácení peněz, dobropis.

Zdroj: Hector Garcia-Molina & Kenneth Salem, Sagas (ACM SIGMOD, 1987); Chris Richardson, Microservices Patterns (2018), kap. 4

Anémický model (Anemic Domain Model – anti-vzor)

Anti-vzor, v němž doménové třídy nesou jen data (gettery a settery) a doménová logika žije v servisních třídách. Porušuje zapouzdření a jde proti bohatému doménovému modelu (Rich Domain Model), na kterém DDD stojí. Z doménových objektů zbudou datové struktury a logika se rozptýlí tak, že ji nikdo nenajde. Martin Fowler ho jako anti-vzor pojmenoval v roce 2003. Často se objevuje i v projektech, které se k DDD hlásí.

Zdroj: Martin Fowler, AnemicDomainModel (2003); Eric Evans, Domain-Driven Design (2003), kap. 5

Primitive Obsession (Primitive Obsession – anti-vzor)

Anti-vzor, kdy doménové koncepty nesou primitivní typy (string, int, float) místo hodnotových objektů. Validace se rozptýlí, protože každý, kdo přijme string s e-mailem, ho musí ověřit sám. Ztrácí se význam a hrozí záměna argumentů: funkce s parametry int $orderId a int $userId přijme hodnoty v opačném pořadí bez chyby typové kontroly. Řešením jsou hodnotové objekty OrderId, UserId, Email a Money, které nesou validaci i význam.

Zdroj: Martin Fowler et al., Refactoring (1999), kap. 3 (code smell); Eric Evans, Domain-Driven Design (2003), kap. 5

Korelační ID (Correlation ID)

Jedinečný identifikátor, který vznikne jednou pro HTTP požadavek nebo pro zprávu, jež proces spustila. Odvozené příkazy a události ho přebírají, vlastní korelační ID negenerují. Umožňuje distribuované trasování, tedy propojení všech operací, které spustila jedna uživatelská akce. V systému s asynchronní komunikací nebo s microservices se bez něj diagnostika neobejde. Mezi službami putuje v HTTP hlavičce (například X-Correlation-ID), v Messengeru ve stampu na envelope. Od něj se liší byznysový korelační klíč ságy (typicky orderId), podle kterého sága pozná, ke které instanci zpráva patří.

Zdroj: Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 8; Gregor Hohpe & Bobby Woolf, Enterprise Integration Patterns (2003)

Causation ID (Causation ID)

Protějšek korelačního ID. Identifikuje konkrétní událost nebo příkaz, který vydání aktuální události přímo způsobil. Korelační ID spojuje celý řetěz od prvotní příčiny, causation ID ukazuje jen bezprostředního předchůdce. Z obou dohromady jde řetěz zrekonstruovat celý: který příkaz co způsobil, jaké události z něj vzešly a které další příkazy tyto události spustily.

Zdroj: Vaughn Vernon, Implementing Domain-Driven Design (2013), kap. 8

05 Pojmy z testování DDD

Testování v kontextu DDD

Dobře oddělený doménový model se dobře testuje. Doménová logika nezávisí na infrastruktuře, takže unit testy běží rychle a deterministicky bez databáze, HTTP i externích systémů. Pojmy v této sekci pokrývají nástroje pro ověřování doménových pravidel, architektury a chování systému při migraci ze starého kódu.

Test double (testovací dvojník)

Obecný termín pro jakýkoli objekt, který v testu nahrazuje skutečnou závislost. Název pochází z filmového prostředí, kde „stunt double“ zastupuje herce v nebezpečných scénách. Gerard Meszaros (xUnit Test Patterns, 2007) rozlišuje pět typů: Dummy, Stub, Fake, Mock a Spy. Každý slouží k něčemu jinému a špatná volba dělá testy křehkými.

Zdroj: Gerard Meszaros, xUnit Test Patterns (2007), kap. 11

Stub (Stub)

Test double, který vrací přednastavené odpovědi bez jakékoli logiky. Volání neověřuje. Jen dodá testovanému kódu hodnotu tam, kde by skutečná závislost sahala do databáze, na síť nebo k jinému externímu zdroji. Příklad: StubExchangeRateProvider vždy vrátí kurz 25 CZK/EUR. Test nezajímá, jestli se stub volal, ale co vrátil.

Zdroj: Gerard Meszaros, xUnit Test Patterns (2007), kap. 11; Martin Fowler, Mocks Aren't Stubs (martinfowler.com, 2004)

Mock (Mock)

Test double, který ověřuje volání: očekává, že se konkrétní metody zavolaly s konkrétními argumenty. Když volání chybí nebo proběhne jinak, test selže. Hrozí přespecifikace, kdy test kontroluje implementační detaily místo chování a láme se při každém refaktoringu. V DDD se mock hodí na volání externích portů, ne na volání uvnitř agregátu.

Zdroj: Gerard Meszaros, xUnit Test Patterns (2007), kap. 11; Martin Fowler, Mocks Aren't Stubs (2004)

Fake (Fake)

Funkční, ale zjednodušená implementace rozhraní. Příklad: InMemoryUserRepository ukládá data do pole v paměti a databázi nepotřebuje. Pro repozitáře je to v DDD výchozí test double. Na rozdíl od mocku neváže testy na implementační detaily. Na rozdíl od stubu opravdu funguje, takže odhalí chyby v logice, která závisí na výsledcích dotazů. Napsat ho dá víc práce, testy ale získají na hodnotě.

Zdroj: Gerard Meszaros, xUnit Test Patterns (2007), kap. 11

Charakterizační test (Characterization Test)

Test, který zachytí současné chování legacy kódu před refaktoringem. Popisuje, co kód dělá, ne co by dělat měl. Při migraci z CRUD na DDD slouží jako záchranná síť: hlídá, aby refaktoring nezměnil pozorovatelné chování systému. Termín zavedl Michael Feathers v knize Working Effectively with Legacy Code (2004). Charakterizační testy jsou dočasné. Po migraci je postupně nahradí testy, které popisují zamýšlené chování.

Zdroj: Michael Feathers, Working Effectively with Legacy Code (2004), kap. 13

Architektonický test (Architectural Test)

Automatizovaný test, který hlídá pravidla závislostí mezi vrstvami nebo moduly. Například: „Doménová vrstva nesmí záviset na infrastruktuře“ nebo „Žádná třída v namespace Domain\ nesmí importovat třídu z Infrastructure\“. Architektonické testy jsou spustitelnou dokumentací architektonických rozhodnutí. Brání erozi architektury, tedy postupnému porušování pravidel, které bez automatické kontroly přijde samo. Nástroje pro PHP: Deptrac a PHPArkitect.

Zdroj: Deptrac (github.com/deptrac/deptrac), PHPArkitect (github.com/phparkitect/arkitect); širší kontext: Mark Richards & Neal Ford, Fundamentals of Software Architecture (2020), kap. 6 „Measuring and Governing Architecture Characteristics“ (fitness functions).

06 Slovní pasti – false friends

Proč to vzniká

Termíny DDD se překrývají s pojmy ze Symfony, jeho bezpečnostní komponenty, z Doctrine i ze světa Springu a Javy. Stejné slovo tak znamená v různých kontextech různé věci a běžné code review rozpor často přehlédne. Následující hesla rozplétají záměny, které se v DDD projektech opakují bez ohledu na tým a framework.

ACL

V DDD: Anti-Corruption Layer, překladová vrstva mezi Bounded Contextem a cizím modelem. Mimo DDD: Access Control List, autorizační seznam (kdo smí na co). V DDD literatuře znamená „ACL“ vždy to první. Detail.

Repository

V DDD: doménová abstrakce, rozhraní v App\<BC>\Domain s metodami get() a save(), které s agregátem zachází, jako by žil v paměti. V Symfony a Doctrine: Doctrine\ORM\EntityRepository, třída s findBy a findAll. Repozitář v DDD je vždy rozhraní, Doctrine EntityRepository je vždy implementace v App\<BC>\Infrastructure.

Service

V DDD: doménová služba (doménové pravidlo bez vlastníka), aplikační služba (koordinace use casu), infrastrukturní služba (adaptér portu). V Symfony: jakákoli třída v DI kontejneru. Název FooService bez určení typu proto nic neříká.

Event

V DDD: doménová událost (OrderPlaced, fakt, který v doméně nastal). V Symfony: objekt předávaný EventDispatcheru, typicky událost frameworku nebo kernelu (KernelEvents::REQUEST). Doménová událost proto nedědí z Symfony\Contracts\EventDispatcher\Event, jinak by doména závisela na frameworku.

Entity

V DDD: objekt s identitou (Customer, Order), buď kořen agregátu, nebo jeho vnitřní část. V Doctrine: třída s atributem #[ORM\Entity] mapovaná na řádek databáze. Entita v DDD může, ale nemusí být zároveň entitou Doctrine.

Domain

V DDD: oblast podnikání, kterou software modeluje (Ordering, Billing, Catalog). V Twigu a překladech: translation domain (messages, validators). Jinde v PHP se slovo objevuje zřídka, záměna proto hrozí hlavně u překladů.

Controller

V Symfony: třída, která obsluhuje HTTP požadavek. V DDD: driving adaptér, který patří do App\<BC>\Infrastructure\Http\, nikoli do Application, natož do Domain.

Handler

V Symfony Messenger: třída s #[AsMessageHandler], kterou message bus zavolá pro danou zprávu. Orchestruje use case, doménovou logiku deleguje na agregát. V DDD: aplikační služba, pokud zpracovává command, nebo handler doménové události, pokud na ni reaguje. Obojí patří do aplikační vrstvy.

Aggregate

V DDD: shluk objektů, které se mění atomicky. V SQL a reportech: agregační funkce (SUM, COUNT). Významy se nepřekrývají, začátečníky ale shoda slov mate.

07 Mapování Symfony ↔ DDD (rozšířená tabulka)

Krátká verze tabulky je v Cheat Sheetu. Tato rozšířená přidává odkazy na kapitoly a anti-vzory.

Symfony konstrukt DDD pojem Umístění v projektu Detail / anti-vzor
Controller (HTTP) Driving adaptér (infrastruktura) App\<BC>\Infrastructure\Http\ Hexagonální architektura – adaptér · Anti-vzor: doménová logika v controlleru
#[AsMessageHandler] handler Aplikační služba (use case) App\<BC>\Application\Handler\ CQRS · doménovou logiku deleguje na agregát
EntityRepository (Doctrine) Repozitář – implementace App\<BC>\Infrastructure\Doctrine\Doctrine<X>Repository.php Rozhraní leží v doméně
Voter Autorizace use casu App\<BC>\Infrastructure\Security\ Autorizace – Voter
Form Mapování vstupu (DTO) App\<BC>\Infrastructure\Http\Form\ Form getData() → command DTO, ne přímo agregát
Twig template View nad read modelem templates/<bc>/ Zobrazuje read model, ne write model
Messenger transport (sync) Command a event bus uvnitř jednoho procesu monolit –
Messenger transport (async, AMQP) Asynchronní zpracování; přes hranici kontextu jen integrační události mezi Bounded Contexty nebo službami Outbox Pattern · DDD a microservices
Console Command Driving adaptér (alternativa k HTTP) App\<BC>\Infrastructure\Cli\ Volá tytéž aplikační služby jako HTTP controller
Doctrine Migration Vývoj databázového schématu migrations/ Nemá ekvivalent v DDD, čistá infrastruktura
EventSubscriber (Doctrine postPersist) Infrastruktura (ne handler doménové události) App\<BC>\Infrastructure\Doctrine\EventListener\ Anti-vzor: doménová pravidla v Doctrine listeneru

★ Primární literatura

Definice v tomto glosáři vycházejí z následujících primárních zdrojů:

  • Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003. ISBN 978-0-321-12521-7
  • Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013. ISBN 978-0-321-83457-7
  • Vernon, Vaughn. Domain-Driven Design Distilled. Addison-Wesley, 2016. ISBN 978-0-13-443442-1
  • Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2002. ISBN 978-0-321-12742-6
  • Young, Greg. CQRS Documents. 2010. Dostupné z: cqrs.files.wordpress.com