DÍL 09a KAPITOLA 09 · SEKCE 09.01–09.04

Hexagonal vs. Onion Architecture v Symfony: kdy co

Proč architektonický styl není totéž co DDD, co umí a neumí vrstvy, jak v Symfony zapojit porty a adaptéry (driving, driven, autowiring, #[AsAlias]) a čím se Onion liší od Hexagonal.

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

Hexagonální struktura kolem prázdné domény

Modelová situace. Tým založí vzornou hexagonální strukturu. Porty v doméně, use casy v aplikační vrstvě, adaptéry v infrastruktuře.

Uvnitř ale zůstal Order se stavem jako obyčejný řetězec, se setterem a getterem. Logika sedí v handleru. Adresáře jsou hotové a doména se nezlepšila vůbec.

Struktura a model jsou totiž dvě nezávislá rozhodnutí.

Architektonický styl není totéž co DDD

Když tým řekne, že přechází na DDD, obvykle tím myslí dvě věci. Bude líp modelovat doménu. A přerovná adresáře.

Jsou to ale dvě nezávislé osy. Na první je modelování, od CRUD modelu přes anémický model po bohatý model s agregáty a invarianty.

Druhá osa určuje uspořádání kódu. Vede od vrstev přes Hexagonal, Onion a Clean až k Vertical Slice.

Posun po jedné ose s druhou nepohne. Hexagonal nad anémickým CRUD modelem existuje, přesně jako v úvodu. A bohatý model s metodami confirm a cancel klidně žije v obyčejné složce Entity.

Evans v knize z roku 2003 věnuje vrstvám jednu krátkou kapitolu. Těžiště leží v modelování a vrstvy model jen chrání.

A triviální doméně bez invariantů žádný styl nepomůže. Není co chránit.

Krátký odhad. Spraví přechod na Hexagonal špatně modelovanou doménu?

Nespraví. Prázdný model zůstane prázdný, jen rozložený do víc adresářů. A bohatý model svázaný s Doctrine zase bez databáze neotestujete.

Layered architektura a Anemic Domain Model

Nejstarší z těch stylů jsou vrstvy. Fowler v Patterns of Enterprise Application Architecture z roku 2002 pracuje se třemi: prezentace, doménová logika a zdroj dat.

Evans o rok později se čtyřmi. Uživatelské rozhraní, aplikace, doména a infrastruktura. Vrstva přitom smí záviset jen na vrstvách pod sebou, nikdy nahoru.

Symfony vám vrstvy dá skoro zadarmo. Skeleton a MakerBundle založí Controller, Entity a Repository a junior se v tom vyzná bez vysvětlování.

Riziko vrstev je známé. Entita zdegeneruje na strukturu pro Doctrine a logika se přestěhuje do služby.

Třeba takhle. OrderService zkontroluje stav getterem a nový nastaví setterem. Fowler tomu v roce 2003 dal jméno Anemic Domain Model.

Pomůže přesunout pravidlo do entity. Metoda confirm sama ověří stav i to, že objednávka není prázdná. To je poctivé modelování, i když třída leží ve složce Entity.

Má to ale háček. Přes atributy ORM entita závisí na Doctrine. Z pohledu Hexagonal nebo Onion je to domain leak. Layered tenhle kompromis přijímá a kniha ho dokonce bere jako výchozí volbu.

Kdy vrstvy přestanou stačit? Když doménu chcete testovat bez databáze. Když měníte perzistenci. Nebo když do aplikace vede víc vstupů.

Krátký odhad. Jak otestujete confirm v typickém Layered controlleru?

Buď přes WebTestCase s bootem celé aplikace, nebo s mocky repozitáře i služby. V hexagonální struktuře stačí zavolat use case, controller k tomu nepotřebujete.

Hexagonal Architecture (Ports & Adapters) v Symfony

Alistair Cockburn popsal hexagonální architekturu v roce 2005. Vzor nese i druhé jméno, Ports and Adapters.

Chtěl aplikační logiku testovat bez uživatelského rozhraní a bez databáze. Jádro proto mluví s okolím jen přes porty a konkrétní technologie se do nich zapojují jako adaptéry.

Šestka v šestiúhelníku přitom nic neznamená. Cockburn jen chtěl kolem jádra dost místa na kreslení portů.

Driving port říká, co aplikace umí. Třeba ForPlacingOrders, předpona For je Cockburnova konvence.

Driven port říká, co aplikace potřebuje. Třeba repozitář objednávek nebo publikaci událostí.

Požadavek pak jde takhle. HTTP controller zavolá driving port, handler za ním založí objednávku a uloží ji přes driven port. Zapíše ji až Doctrine adaptér.

Bacha na směr šipek. Volání teče zvenku dovnitř a zase ven, závislosti ale míří jen dovnitř. Doctrine adaptér implementuje rozhraní z jádra a jádro o Doctrine neví.

Kolik portů je správně? Cockburn port definuje jako konverzaci s jedním účelem, do které se typicky zapojí víc adaptérů. Sám se kloní ke dvěma až čtyřem portům na aplikaci.

PHP praxe jde jemněji a každá závislost dostane vlastní rozhraní, kvůli autowiringu a testovacím náhradám. Kniha to dělá taky, jen to není Cockburnova definice.

Krátký odhad. Rozhraní RedisOrderCache. Je to port, nebo porušení vzoru?

Porušení. Cockburn v rozhovoru z roku 2020 označil port na technologii za hlavní chybu praxe. Obstojí OrderCache s adaptérem pro Redis a druhým v paměti pro testy.

V kódu má HTTP controller v konstruktoru jen rozhraní ForPlacingOrders. Z požadavku sestaví vstupní DTO a předá ho portu.

Za portem stojí PlaceOrderHandler. Primitivy z DTO převede na hodnotové objekty, založí objednávku přes Order place a uloží ji přes driven port.

Jak se port potká s adaptérem? Když Symfony při načítání služeb najde rozhraní a právě jednu implementaci, alias vytvoří samo.

Ručně ho zapíšete, když je implementací víc, nebo když adresář s rozhraním či implementací vypadl z resource. To druhé znáte z minulého dílu.

Lákavá zkratka je atribut Autowire s Doctrine repozitářem přímo v konstruktoru handleru. Funguje to, jenže aplikační vrstva pak importuje infrastrukturu. A to porušuje Dependency Rule.

Handler proto zůstane u prostého rozhraní. Alias patří do services.yaml, nebo jako atribut AsAlias přímo na adaptér, který tím označíte za výchozí implementaci portu.

Stejně funguje port pro publikaci událostí. V produkci za ním stojí Messenger, v testech InMemoryEventPublisher, který události jen sbírá do pole. Takový test běží v jednotkách milisekund.

A tým z úvodu? Struktura byla hexagonálně v pořádku, jen jí chyběl model. A u CRUD modelu nad pár tabulkami jsou porty prostě režie navíc.

Onion Architecture a rozdíl proti Hexagonal

Onion Architecture představil Jeffrey Palermo v roce 2008 v sérii článků. Čtvrtý díl přidal v roce 2013.

Onion staví doménový model doprostřed. Kolem něj jsou doménové služby, pak aplikační služby a úplně venku uživatelské rozhraní s infrastrukturou.

Závislosti smějí mířit jen dovnitř. Jméno Dependency Rule tomu pravidlu dal až Robert Martin.

Proti Evansovým vrstvám je to změna. Tam doména smí záviset na infrastruktuře pod sebou. Onion to zakazuje a repozitáře jádro jen deklaruje jako rozhraní.

Palermo mimochodem odmítá výklad, že jde o architekturu pro DDD. Podle něj funguje s DDD vzory i bez nich.

Čím se liší od Hexagonal? V běžné implementaci v Symfony skoro ničím, obě obracejí závislosti dovnitř. Rozdíly jsou tři a jemné.

Onion dělí vnitřek na dvě vrstvy služeb, doménové a aplikační. Hexagonal žádné vnitřní vrstvení nepředepisuje.

Liší se i pohled. Onion kreslí, kdo na kom závisí, kdežto Hexagonal sleduje cestu dat porty a adaptéry.

A controller s Doctrine repozitářem jsou v Onion ve stejné vnější vrstvě. Hexagonal je rozlišuje. Driving adaptér aplikaci volá, driven adaptér volá aplikace.

Porty a adaptéry se dvěma vrstvami služeb uvnitř jsou prostě hybrid obou stylů. To je v pořádku, málokdo drží jeden styl čistě.

Jak ty dvě vrstvy služeb vypadají v kódu? Vezměme výpočet ceny košíku v jedné třídě. Načte košík, zákazníka a slevová pravidla a rovnou počítá.

Výpočet pak bez repozitářů otestovat nejde. Onion ho proto vytáhne do doménové služby PriceCalculator, která repozitář nezná.

Aplikační služba CalculateCartPrice pak jen načte agregáty přes porty a výpočet předá dál.

Krátký odhad. Kterou z těch dvou tříd otestujete bez databáze?

PriceCalculator. Dostane objekty v paměti a stačí mu obyčejný unit test. CalculateCartPrice potřebuje aspoň repozitáře v paměti.

Otázky a shrnutí

Krátká otázka na rozmyšlenou. Kdy je v Symfony potřeba zapsat alias portu ručně?

Když má port víc implementací. Nebo když adresář s rozhraním či implementací vypadl z resource.

A jedna z dílu o hranicích agregátu. Proč handler předává do Order place jen CustomerId, a ne celý objekt zákazníka?

Agregáty se odkazují přes identitu. Objektová reference svádí k řetězené úpravě a v jedné transakci se pak mění dva agregáty.

Takže v kostce. Styl a DDD jsou dvě nezávislá rozhodnutí a struktura prázdný model nezachrání.

Hexagonal vede porty podle konverzace a technologii nechává adaptérům. Onion přidává dělení na doménové a aplikační služby, v Symfony ale vypadá skoro stejně.

Příští díl přehled dokončí. Přijde Clean Architecture, Vertical Slice a hlavně volba mezi styly. Celou kapitolu najdete v knize na ddd-v-symfony.katuscak.cz.

← Všechny díly videokurzu