DÍL 06b KAPITOLA 06 · SEKCE 06.05–06.09

Agregát, repozitář a doménové události v Symfony

Agregát s kořenem a pojmenovanou továrnou, úzký repozitář, doménová služba bez stavu a doménové události: kde je zaznamenat, kdy je publikovat a proč nikdy v konstruktoru.

Přehrávač YouTube se načte až po kliknutí · otevřít na YouTube (nové okno) ↗

Přepis dílu komentář tak, jak zazní ve videu

Objednávka, která vzniká pořád znovu

Modelový e-shop načte tutéž objednávku třikrát. A třikrát se dozví, že objednávka právě vznikla.

Stačí jedno volání record na špatném místě, prostě v konstruktoru. Metoda, která objednávku skládá z uložených dat, konstruktor volá taky. A s ním i record.

Agregát a kořen agregátu

Objednávka má položky, stav a celkovou částku. Změna položky znamená přepočet částky, storno zase kontrolu stavu. Když tahle pravidla nehlídá jedno místo, rozlezou se po službách.

Vypadá to pak třeba takhle. Kód zvenčí sáhne přímo do kolekce položek a stav nastaví setterem. Jestli objednávku ještě jde měnit, tady nekontroluje nikdo.

Agregát to řeší jasnou hranicí. Tvoří ho skupina objektů, které se mění jako jeden celek a sdílejí stejné invarianty, tedy pravidla, která platí pořád. Dovnitř se vstupuje jen přes kořen, tady přes Order.

Evans k tomu doporučuje, aby se hranice agregátu kryla s hranicí transakce. Do samotné definice to ale nepatří. Co z toho plyne pro návrh, přijde na řadu v dalších dílech.

Jak to vypadá v kódu? Konstruktor je privátní a instance vzniká pojmenovanou továrnou place. Ta dostane OrderId a CustomerId z minulého dílu. Konstruktor pak nastaví stav na rozpracovaný. Objednávka bez zákazníka nebo bez počátečního stavu tak vůbec nevznikne.

Položky přibývají metodou addItem. Ta nejdřív ověří, že je objednávka pořád rozpracovaná. Pokud není, hodí pojmenovanou výjimku pro nepovolený přechod stavu.

Samotnou položku přitom zvenčí nikdo nevytváří ani nemění. Založí ji jen addItem na kořeni.

Metoda confirm hlídá stav stejně jako addItem a přidává druhé pravidlo. Prázdnou objednávku potvrdit nejde a metoda hodí vlastní pojmenovanou výjimku. Volající se pak rozhoduje podle typu výjimky a text zprávy číst nemusí.

K téhle verzi dvě poznámky. Order zatím nedědí z žádné bázové třídy a události nezaznamenává, to přijde v poslední části. A na třídě není ani jeden atribut Doctrine, takže model přečtete, i když Doctrine neznáte.

Stav drží enum OrderStatus. Pro uzavřený výčet stavů bez další logiky plný hodnotový objekt potřeba není.

Repozitář

Doména nemá vědět, jestli objednávka leží v PostgreSQL, v MongoDB, nebo jen v paměti. O to se postará repozitář. Pro doménu se tváří jako kolekce agregátů v paměti.

Vzor pochází z katalogu Patterns of Enterprise Application Architecture, kde ho popsali Edward Hieatt a Rob Mee.

Rozhraní je záměrně úzké. Metoda save objednávku uloží a get ji načte podle identity.

Když objednávka neexistuje, get hodí pojmenovanou výjimku a null nevrací nikdy. Chybějící objednávka je totiž chyba volajícího.

Implementace žije v infrastruktuře. Nejčastěji ji obstará Doctrine, pro testy stačí varianta v paměti.

Od obyčejné třídy nad databází ho oddělují tři pravidla. Na každý kořen agregátu připadá jeden repozitář. Položka objednávky vlastní nemá, načítá se a ukládá spolu s objednávkou.

Dál vrací sestavené agregáty, nikdy řádky ani pole. Tím se liší od DAO, které pracuje s tabulkami.

Třetí pravidlo si zkuste odhadnout. Patří do OrderRepository metoda, která najde všechny objednávky zákazníka?

Nepatří. Dotazy pro obrazovky obsluhuje read model a k němu se dostaneme v dalších dílech.

A proč vlastně metoda save? Vernon popisuje dvě podoby repozitáře. Collection-oriented se chová jako kolekce a spoléhá na to, že persistence sleduje změny sama. Persistence-oriented má save a hodí se tam, kde úložiště změny nesleduje.

Doctrine změny sleduje, takže by obstála i první podoba. Kniha přesto volí save, protože je v kódu vidět, kde se zapisuje.

Doménová služba

Některá pravidla nepatří jednomu agregátu. Třeba doprava zdarma pro VIP zákazníky a pro velké objednávky.

To pravidlo čte data dvou agregátů. Customer o dopravném nic neví a Order nezná věrnostní status zákazníka.

Obě znalosti proto spojí doménová služba. Nedrží žádný stav a pracuje jen s objekty, které dostane.

Metoda feeFor vrátí nulu, když je zákazník VIP, nebo když má objednávka aspoň pět řádků. Jinak účtuje paušál devadesát devět korun. Pozor, počítají se řádky objednávky, ne kusy zboží.

Proč to není metoda na agregátu? Výpočet celkové částky na Order zůstává, protože pracuje jen s jeho daty. Služba má smysl až tam, kde logika přesahuje hranici jednoho agregátu.

Evans službu popisuje jako operaci se samostatným rozhraním. Ukázka z ní přesto PHP interface nedělá. Implementace je jediná a abstrakce navíc by přidala jen další soubor. Doplnit se dá kdykoli, třeba až bude potřeba službu v testu nahradit.

Teď krátký odhad. Pravidlo, že zaplatit jde jen potvrzenou objednávku. Patří do doménové služby PaymentService?

Nepatří. Je to invariant agregátu Order a ten si ho hlídá sám. Vytvoření platby pak obstará továrna přímo na třídě Payment.

Všimněte si jejích parametrů. Dostane jen identifikátor objednávky a částku. Mezi agregáty putují identifikátory a hodnotové objekty, reference na jiný agregát nikdy.

Doménová událost

Objednávka byla potvrzena. Platba byla přijata. Doménová událost je neměnný záznam o něčem, co se v doméně stalo a o čem chtějí vědět doménoví experti.

Proto se jmenuje v minulém čase. OrderPlaced popisuje věc, která už se stala. Evans dodává, že událost obvykle nese čas, kdy nastala, a identitu entit, kterých se týká.

Vlastnosti jsou veřejné a jen pro čtení, protože příjemci událost jen čtou.

Kolik dat do události dát, je potřeba rozhodnout. Tenká událost nese jen identifikátory a zbytek si příjemce dotáhne sám. Když událost veze celý stav, mluví Fowler o Event-Carried State Transfer.

Uvnitř jednoho kontextu se osvědčí tenká varianta. Příjemce má k agregátu přístup a kopie dat by se dřív nebo později rozešly.

Doménová událost navíc mluví jazykem Orderingu a zůstává uvnitř kontextu. Kdo ji pošle ven, zveřejní svůj vnitřní model. Cizím kontextům patří integrační událost přeložená do Published Language.

Lifecycle: record, save, releaseEvents

Cesta události k příjemcům má dva kroky. Agregát ji zaznamená v doménové metodě, ve chvíli, kdy se změna stane. Publikuje ji až aplikační vrstva, a to teprve po uložení.

Mezi oběma kroky drží události bázová třída AggregateRoot. Metoda record událost přidá do seznamu. A releaseEvents seznam vydá a rovnou ho vyprázdní.

Order teď z AggregateRoot dědí. Továrna place objednávku založí a hned zaznamená OrderPlaced.

Bacha na význam. OrderPlaced říká, že objednávka vznikla. Pořád je rozpracovaná a o potvrzení se postará až confirm, který zaznamená vlastní událost OrderConfirmed.

A teď zpátky k úvodu dílu. Proč record nepatří do konstruktoru? Konstruktor volá i reconstitution, tedy sestavení agregátu z uložených dat.

Doctrine ho při hydrataci obchází, takže s ní by se chyba hned neukázala. Vlastní metoda reconstitute ho ale zavolá a každé načtení by ohlásilo vznik objednávky znovu.

Druhou polovinu obstará command handler. Objednávku založí a přes repozitář uloží. Flush zapíše SQL a teprve pak handler vyzvedne události přes releaseEvents a pošle je na event bus.

Commit ale flush neudělá. Transakci otevře middleware doctrine transaction před handlerem a potvrdí ji, až handler doběhne. Dispatch tedy běží uvnitř otevřené transakce.

Krátký odhad. Co by se stalo, kdyby handler události publikoval ještě před flush?

Příjemci by se dozvěděli o změně, kterou databáze mohla odmítnout. Bez middlewaru, kdy commit obstará sám flush, hrozí zase opak. Když proces spadne mezi uložením a publikací, událost se ztratí.

Synchronním posluchačům ve stejném procesu dispatch v transakci nevadí. Když transakce spadne, zmizí i jejich zápisy. Co ale opouští proces, třeba zpráva do brokeru nebo e-mail, jde přes outbox. K němu se dostaneme v dalších dílech.

Otázky a shrnutí

Krátká otázka na rozmyšlenou. Proč volání record nepatří do konstruktoru agregátu?

Konstruktor volá i reconstitution. Každé sestavení objednávky z uložených dat by znovu zaznamenalo OrderPlaced. Proto record patří do továrny a do doménových metod.

A jedna z dílu o Event Stormingu. Pod papírkem agregátu stojí invariant: prázdnou objednávku nejde potvrdit. Co z něj zbude v kódu?

Výjimka v doménové metodě a jeden test. Tu výjimku jste dnes viděli v metodě confirm.

Takže v kostce. Agregát hlídá svá pravidla na jednom místě a vzniká pojmenovanou továrnou.

Repozitář je úzký a na každý kořen připadá jeden. Dotazy pro obrazovky do něj nepatří. A událost se zaznamená v doménové metodě, publikuje se až po uložení.

Příští díl je o návrhu agregátu, tedy o tom, jak podle invariantů vést jeho hranici. Celou kapitolu o základních konceptech najdete v knize na ddd-v-symfony.katuscak.cz.

← Všechny díly videokurzu