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.