DÍL 08b KAPITOLA 08 · SEKCE 08.03–08.05

Domain Service, Factory a Module v Symfony

Kdy má doménová služba vzniknout a kdy je to handler v přestrojení, kdy nestačí named constructor a přichází Factory class, proč reconstitute nekontroluje invarianty a jak moduly podle Bounded Contextů uhlídat v CI.

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

Doménová služba, která doménová není

Ve složce doménových služeb sedí třída OrderService. V konstruktoru si bere EntityManager, repozitář objednávek a dispatcher událostí.

Metoda cancel objednávku načte, zruší, uloží a rozešle události. Podle složky je to doménová služba. Jenže bez databáze ji neotestujete a doménové pravidlo v ní nenajdete žádné.

Domain Service: kdy ano a kdy ne

Doménovou službu znáte z dílu o agregátu. ShippingFeeService tam spojila data zákazníka a objednávky. Teď jde o to, kdy má služba vůbec vzniknout.

Evans ve své knize z roku 2003 uvádí tři kritéria. Operace se týká doménového pojmu, ale přirozeně nepatří žádné entitě ani hodnotovému objektu. Její rozhraní tvoří jiné prvky modelu. A nemá vlastní stav.

Učebnicovým příkladem je převod peněz mezi dvěma účty. Ani jeden účet nezná ten druhý, takže operaci vlastnit nemůže. Podobně funguje výpočet ceny z pravidel, segmentu zákazníka a kupónu.

Je to ale nejčastěji zneužívaný vzor v DDD. Tým založí OrderService a přestěhuje do ní logiku z entit. Entitám zbudou jen data a vznikne anémický model.

Proto kapitola nabízí tři kontrolní otázky. Patří operace do nějaké entity? Je to doménové pravidlo, nebo koordinace? A není to spíš infrastruktura, třeba e-mail?

Zkusme je na třídu z úvodu. Pravidla storna už hlídá Order v metodě cancel. Zbytek je načtení, flush a rozeslání událostí, tedy koordinace.

Je to Application Service v přestrojení. Stačí ji přesunout do aplikační vrstvy jako CancelOrderHandler a doménová služba tu žádná nevznikne.

Pravidla storna teď otestujete na samotné objednávce, bez databáze. A kapitola přidává obecný test. Doménová služba musí projít bez Symfony kernelu. Když to nejde, zůstal v ní kus infrastruktury.

MoneyTransferService a tři druhy služeb

Jak vypadá služba, která obstojí? Kapitola bere Evansův převod peněz. Metoda transfer dostane oba účty, částku, referenci a čas.

Ověří, že zdrojový účet má dost prostředků, jinak hodí doménovou výjimku. Pak z jednoho účtu odepíše a na druhý připíše.

Nic si nedrží a nic neukládá. Oba účty uloží až handler, který službu zavolal.

Jedna výhrada. Služba mění dva agregáty v jedné transakci. Kapitola o návrhu agregátu proto tentýž převod uvádí jako anti-vzor. Obstojí jen jako vědomá výjimka.

Jinak si služba nechá jen rozhodnutí o prostředcích a měně. Samotný převod rozloží sága na dvě transakce.

Smí mít doménová služba v konstruktoru repozitář? Zákaz to není a zdroje se rozcházejí. Nejdřív se vyplatí zkusit, jestli data nedodá volající. Když to nejde, třeba u pravidla nad celou kolekcí, obstojí závislost na doménovém rozhraní.

O zařazení rozhoduje doménové pravidlo uvnitř třídy. Počet parametrů konstruktoru o něm nic neříká.

Sufix Service se v kódu objeví skoro vždycky, jenže skrývá tři role. Doménová služba drží pravidlo bez vlastníka, perzistenci nevolá a stačí jí čistý unit test.

Aplikační služba koordinuje use case a ukládá přes repozitář. Obvykle se jmenuje Handler nebo UseCase.

Infrastrukturní služba obstarává integraci s databází, e-mailem nebo platební bránou. Ověřuje ji integrační test a jmenuje se třeba Gateway nebo Adapter.

Krátký odhad. Hash hesla při registraci. Doménová, aplikační, nebo infrastrukturní služba?

Infrastrukturní, stejně jako e-mail nebo volání cizího API. A doménová služba sufix Service mít nemusí. Často ji líp pojmenuje samotný doménový pojem, třeba FundsTransfer.

Factory: named constructor a Factory class

Pojmenovanou továrnu place a privátní konstruktor znáte z dílu o agregátu. Evans ale vede Factory jako samostatný vzor. Patří tam, kde vznik objektu potřebuje vlastní pravidla.

Kapitola jmenuje čtyři situace. Validace už při vzniku. Polymorfní vznik z různých vstupů. Dohledání zákazníka podle e-mailu. A spletité mapování z DTO.

A kdy ne? OrderFactory, která jen zavolá new, je vrstva navíc. Výběr třídy podle řetězce přijde o typovou kontrolu. A továrna pro každý objekt je přestřelená.

Nejčastější podobou je named constructor, tedy pojmenovaná továrna přímo na agregátu. Kapitola ukazuje variantu s metodou placePhysical, která bere rovnou seznam položek. Prázdná objednávka tak vůbec nevznikne.

Kanonický Order přidává položky přes addItem a prázdnou objednávku zastaví až confirm. Obě cesty jsou obhajitelné.

Vedle stojí placeDigital pro digitální zboží a fromImport pro import. Každá má jiná pravidla a výsledkem je pořád Order, bez dědičnosti. Metodě fromImport navíc volající předá službu CustomerLookup parametrem.

Z košíku je to horší. Vznik potřebuje repozitář košíků, výpočet ceny a hodiny. Proto vzniká samostatná třída OrderFromCartFactory a závislosti jí dodá container.

Uvnitř načte košík, ocení položky a zavolá zase placePhysical. Pravidlo o aspoň jedné položce tak zůstává v agregátu.

Proč tu repozitář nevadí? Podle DDD Reference factory nemusí mít v modelu vlastní odpovědnost, a přesto patří k doménovému návrhu. Hranice leží jinde. Jakmile by výsledek uložila nebo publikovala událost, je z ní command handler.

Odkud se statické place vzalo? Vernon staví do popředí factory metodu na kořeni agregátu. U něj je to ale instanční metoda, která vyrábí jiný agregát. Třeba fórum založí diskusi.

Privátní konstruktor se statickou metodou je konvence PHP komunity, nejčastěji s odkazem na Mathiase Verraese z roku 2014. PHP dovolí jen jeden konstruktor. A vnitřek třídy pak jde měnit bez zásahu do volajících.

Třetím druhem továrny je reconstitute, obnovení agregátu z perzistence. S Doctrine ho obstará hydrátor. Vlastní mapper nebo Event Sourcing ale takovou metodu potřebují.

Invarianty nekontroluje a události nezaznamenává, protože se nic nového nestalo. Značka internal říká, že ji smí volat jen infrastruktura repozitáře. Handler, který by ji použil místo place, by invarianty obešel.

Module: struktura, services.yaml, hranice v CI

Symfony skeleton a MakerBundle zakládají složky podle technické role. Týmy k nim přidají složku Service na všechno ostatní.

Zkuste podle těch složek odhadnout, co aplikace dělá. E-shop, banka, nebo pojišťovna?

Poznat se to nedá. Tomuhle členění se říká type packaging a doménu schovává.

Evansův Module ho obrací. Balíčky se jmenují podle Ubiquitous Language a jeden modul je nejčastěji jeden Bounded Context.

Když moduly zůstanou silně provázané, radí DDD Reference změnit model. Přísnější pravidla v nástroji to nespraví.

Do composer.json kvůli modulům sahat nemusíte. Výchozí PSR-4 mapování App na src stačí, namespace se prostě překládá na podadresáře.

Services.yaml po modulech vypisovat taky netřeba. Vyplatí se jen tehdy, když chcete doménovou vrstvu z containeru vyloučit. Agregáty ani hodnotové objekty totiž nikdo neinjektuje.

Doménové služby a factory třídy se ale injektují, takže se registrují zvlášť. A bacha, vyloučený adresář vypadne i z automatických aliasů. Alias portu, třeba OrderRepository, pak musíte zapsat ručně.

Konvence sama nevydrží, stačí pár sprintů pod tlakem hotfixů. Hranice proto hlídá test, který při porušení shodí build v CI.

Typicky jde o tři pravidla. Ordering nezávisí na jiném kontextu, kromě identifikátorů. Doména neimportuje Symfony ani EntityManager, až na pár vědomých výjimek. A aplikační vrstva nezná infrastrukturu.

Pro PHP existují dva nástroje, PHPArkitect a Deptrac. Do hotového projektu se PHPArkitect nasazuje přes baseline. Starý dluh pak build neshodí, nové porušení ano.

Otázky a shrnutí

Krátká otázka na rozmyšlenou. OrderFromCartFactory má v konstruktoru repozitář a zůstává v doméně. Kdy by z ní přestala být továrna?

Ve chvíli, kdy by výsledek sama uložila nebo publikovala událost. Pak je z ní command handler a patří do aplikační vrstvy.

A jedna z dílu o agregátu. Kde se doménová událost zaznamená a kdo ji publikuje?

Zaznamená ji agregát v továrně nebo v doménové metodě přes record. Publikuje ji handler až po uložení, přes releaseEvents. Přesně to dnes dělal CancelOrderHandler.

Takže v kostce. Doménová služba drží pravidlo, které nemá vlastníka. Perzistenci nevolá a koordinace patří do handleru.

Továrna je nejčastěji named constructor s privátním konstruktorem. Samostatná třída přichází až s injektovanými závislostmi. A modul odpovídá Bounded Contextu, jeho hranici hlídá test v CI.

Příští díl otevírá architekturu. Na řadu přijdou styly Layered, Hexagonal a Onion a vrátí se i ruční aliasy portů. Celou kapitolu o doplňujících taktických vzorech najdete v knize na ddd-v-symfony.katuscak.cz.

← Všechny díly videokurzu