DÍL 09b KAPITOLA 09 · SEKCE 09.05–09.09

Clean Architecture a Vertical Slice v Symfony 8

Co Clean Architecture přidává k Hexagonal a Onion a proč je její use case v Symfony s Messengerem prostě command handler. Jak Vertical Slice přeskládá src/ podle funkcí, proč doménový model zůstává sdílený, jak styl vybrat podle subdomény a šest anti-vzorů napříč styly.

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

Šest souborů jedné změny

Modelová situace. Změna jedné funkce objednávky a diff sahá do šesti souborů ve čtyřech vrstvách. Controller, DTO, aplikační služba, doménová služba, rozhraní repozitáře a jeho implementace.

Vertical Slice z nich tři přestěhuje do jednoho adresáře pro tu funkci. Zbylé tři nechává kniha záměrně venku. Proč zrovna ty?

Clean Architecture

Robert C. Martin v roce 2012 napsal článek The Clean Architecture. Shrnul v něm, co mají společného Hexagonal, Onion a dva starší přístupy, DCI a BCE od Ivara Jacobsona.

Článek o pět let později rozvedl do knihy. Uprostřed leží entity, kolem nich use casy, pak adaptéry rozhraní a na okraji frameworky a ovladače.

Pravidlo, že závislosti ve zdrojovém kódu míří jen dovnitř, tu dostalo jméno Dependency Rule. Čtyři kruhy přitom nejsou povinný počet. Podle Martina jich aplikace může potřebovat víc.

Druhé pravidlo hlídá, co hranici překročí. Projdou jen jednoduché datové struktury, Doctrine entita ani řádek z databáze ne.

Co Clean přidává? Hexagonal mluví o vstupních portech a Onion o aplikačních službách. Clean dělá z use casu samostatný pojem s vlastní třídou a vlastním DTO na vstup i výstup. Kód tak sám tvoří seznam toho, co aplikace umí.

Takhle vypadá use case z kapitoly. Metoda execute dostane vstupní DTO s identifikátorem zákazníka a položkami. Žádný Symfony Request, žádná entita.

Uvnitř pracuje kanonický Order. Vznikne továrnou place, zákazník do něj jde jen přes identifikátor a položky přidává addItem.

Pak use case objednávku uloží a vydá zaznamenané události. Publikuje je synchronně. Kapitola sama upozorňuje, že v produkci sem patří Outbox, jinak se událost při pádu mezi zápisem a publikací ztratí.

Ven jde zase jen DTO s identifikátorem, stavem a částkou. Controller, konzolový příkaz i test volají stejný kontrakt.

Zkuste odhadnout. Čím se takový use case liší od command handleru?

Skoro ničím, hlavně jménem. V DDD je to aplikační služba, v CQRS command handler a Martin takové třídě říká interactor.

V Symfony s Messengerem jako command busem stačí přejmenovat. Ze vstupního DTO je command PlaceOrder a z use casu PlaceOrderHandler. Nová vrstva nepřibude.

Clean se vyplatí tam, kde má každá schopnost vlastní jméno a kontrakt a kde se sejde víc vstupních kanálů. U pár desítek endpointů se přelévání mezi DTO nezaplatí. Nad tenkou doménou z use casů zbude rituál.

Vertical Slice Architecture

Vertical Slice Architecture popsal Jimmy Bogard v roce 2018. Každá funkce v ní dostane vlastní adresář se vším, co potřebuje.

Tady jsou soubory z úvodu jako strom src. Leží ve čtyřech vrstvách a změna jedné funkce projde všemi.

Po přeskládání leží v adresáři PlaceOrder command, jeho handler i controller. Recenzent tak vidí celý use case na jednom místě.

Bogard to shrnuje jedním pravidlem. Mezi řezy co nejmíň vazeb, uvnitř řezu klidně co nejvíc.

Domain a Infrastructure ale zůstaly stranou. Leží v nich ty tři soubory z úvodu.

Bogard jde dál. Vadí mu povinný řetěz, kdy controller musí volat službu a ta repozitář. Uvnitř řezu podle něj většina abstrakcí odpadá. Triviální řez zůstane transakčním skriptem, složitý dostane bohatý model.

Kniha je tu vědomě měkčí. Doménový model zůstává sdílený v celém Bounded Contextu. Řezy dělí jen aplikační a prezentační vrstvu a část infrastruktury.

Proč? Zkuste odhadnout, co by se stalo, kdyby si každý řez modeloval objednávku po svém.

Rozešly by se invarianty. Agregát, který má v každém řezu jinou podobu, přestane být jediným místem, kde doménová pravidla platí.

Vertical Slice a Hexagonal si přitom nekonkurují. Řezy určují hranice mezi funkcemi, Hexagonal, Onion a Clean hlídají závislosti uvnitř nich. Tam může být vlastní port a adaptér. Triviálnímu řezu stačí jediná třída.

Volba stylu a hybridní přístup

Tak který styl vybrat? Čísla v rozhodovací matici kapitoly jsou orientační odhad autora, žádné měření. Víc řeknou tři otázky.

Kolik bude use casů za rok? Do pár desítek stačí Layered. Se stovkami se vyplatí Hexagonal nebo Vertical Slice, velký katalog ukazuje na Clean nebo hybrid.

Vyplatí se oddělit doménu od Doctrine? Když chcete testy bez databáze nebo výměnu persistence, pak ano. Když Doctrine zůstane natrvalo, vystačíte s Layered.

A kolik vstupů aplikace má? Pro samotné HTTP je Layered v pořádku. Když se sejde HTTP, konzole a Messenger, vyplatí se Hexagonal nebo Clean.

Pro středně velký projekt kniha doporučuje výchozí kombinaci. Hexagonal a Vertical Slice, k tomu CQRS přes Symfony Messenger. Přesně ten strom jste viděli ve druhé části.

Univerzální pravda to ale není. Interní administraci s dvaceti endpointy stačí Layered ze Symfony skeletonu a tým s ní nejspíš iteruje rychleji.

Proč vlastně jeden styl pro celý projekt? Kniha rozděluje investici podle typu subdomény, jak je znáte z dílu o subdoménách.

V modelovém e-shopu nese konkurenční výhodu tvorba cen a slev. Pricing je Core, takže dostane plný Hexagonal s bohatým modelem a porty.

Správa objednávek je Supporting. Stačí jí Layered, tedy controller, služba, entita a repozitář.

Odhadněte sami. Kolik vrstev si zaslouží notifikace přes SendGrid?

Skoro žádnou. Notifikace jsou Generic, takže z nich zbude tenký adaptér nad SendGridem nebo Twiliem. Bez agregátů a bez doménových služeb.

Jedno pravidlo se přitom neporušuje. Uvnitř jednoho Bounded Contextu se styly nemíchají. Kontext napůl v Hexagonal a napůl v Layered hybrid není.

Cena? Tým musí umět víc stylů, někdo zkušený je musí hlídat a konvence se mezi kontexty liší.

Anti-vzory napříč styly

Na závěr šest anti-vzorů. Všechny vznikají až při provedení, když je styl dávno vybraný.

První je Hexagonal kult. Tým přečte Cockburna a každý CRUD endpoint dostane port a adaptér. Načtení jednoho produktu podle ID pak zabere šest souborů místo dvou.

Náprava je prostá. Hexagonal patří tam, kde je doménová logika. Pro čistý CRUD bez invariantů stačí controller s přímým dotazem přes Doctrine.

Druhý je Doctrine v doméně. Tým má port OrderRepository, jenže Order nese atributy Doctrine a na knihovně dál závisí. Bez potřeby hexagonální hranice je to obhajitelný kompromis. Jinak pomůže persistenční třída s mapperem.

Třetí je Anemic Hexagonal. Porty jsou vzorné, model má jen gettery a settery a logika sedí v handlerech. Tady se nejdřív opravuje modelování.

Čtvrtý je port pojmenovaný po technologii, třeba RedisOrderCache. Cockburn to považuje za hlavní chybu, kterou u svého vzoru vídá. Port se jmenuje podle konverzace domény, třeba OrderCache.

Pátý je předčasná inverze závislostí. Rozhraní má smysl, když závislost chcete v testu nahradit, nebo když implementací bude víc.

Zkuste to na konkrétním případu. Rozhraní CustomerRepository, jediná implementace a žádná testovací náhrada. Nechat?

Podle kapitoly ne. Neplatí ani jeden z obou důvodů, takže rozhraní jen přidá skok navíc při čtení kódu.

Šestý je architecture astronaut. Tým měsíce ladí dokonalou architekturu a uživatel čeká na první funkci. Test je prostý. Když za pět minut nevysvětlíte, jaký reálný problém abstrakce řeší, nejspíš žádný.

Otázky a shrnutí

Otázka na rozmyšlenou. Nový kolega chce v kontextu zákazníků, který běží v Layered, napsat jednu funkci hexagonálně. Hybrid, nebo chyba?

Chyba. Hybrid znamená různé styly pro různé kontexty. Uvnitř jednoho kontextu se styly nemíchají.

A jedna z dílu o entitách. Use case volá OrderId generate ještě před uložením. Proč ID nevzniká až v databázi?

Entita s ID od databáze ho při vzniku nemá. Nemohla by ohlásit událost o svém vzniku a nikdo by se na ni nemohl odkázat.

Takže v kostce. Use case z Clean Architecture je v Symfony s Messengerem prostě command handler.

Vertical Slice řeší hranice mezi funkcemi, Hexagonal závislosti uvnitř nich. Doménový model přitom zůstává sdílený v celém kontextu.

A styl se řídí subdoménou. Plný Hexagonal patří do Core a vrstva, která projektu nic nevrací, může jít pryč.

Příští díl přechází k implementaci v Symfony. Agregát User a jeho hodnotové objekty dostanou mapování v Doctrine. Celou kapitolu o architektonických stylech najdete v knize na ddd-v-symfony.katuscak.cz.

← Všechny díly videokurzu