DÍL 08a KAPITOLA 08 · SEKCE 08.01–08.02

Specification Pattern: pravidla bez vnořených if

Specification Pattern v PHP 8.4 a Symfony 8: doménové pravidlo jako pojmenovaný objekt, skládání přes and, or a not místo vnořených ifů, kdy se vzor nevyplatí a jak tutéž specifikaci přeložit do dotazu přes Doctrine Criteria.

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

Doprava zdarma ve třech vnořených ifech

Modelový e-shop. Marketing chce dopravu zdarma od tisíce korun, jen v EU a ne pro zákazníky na blacklistu. V kódu z toho jsou tři vnořené ify.

Jinde přitom platí prémiový zákazník v EU a o kus dál prémiový zákazník nebo slevový kód. Každá kombinace je další vnořený if a žádné pravidlo nemá jméno.

Specification pattern z nich udělá pojmenované kousky, které se skládají do jednoho čitelného výrazu.

Co je Specification

Většina výkladů DDD končí u agregátu. Zbylé Evansovy vzory pak působí jako abstrakce navíc.

Totéž přece jde napsat ifem, statickou metodou nebo obyčejnou složkou v src.

V netriviální doméně ale bez nich agregáty bobtnají a pravidla se opakují. Dnes první z nich, specifikace.

Specifikace je samostatný objekt s jedním booleovským predikátem nad doménovým objektem. Rozhraní má jedinou metodu, isSatisfiedBy, a ta vrátí true, nebo false.

Na rozhraní nic složitého není, podstatné je pojmenování. Doménový expert řekne, že objednávka má nárok na dopravu zdarma, a přesně tak se jmenuje i třída.

Uvnitř je jediná podmínka. Celková částka musí být ve stejné měně jako limit a musí ho dosáhnout.

Pravidlo tak přestává být kombinací ifů někde v servisní vrstvě. Stává se pojmenovaným prvkem Ubiquitous Language.

Vzor popsali Eric Evans a Martin Fowler v článku Specifications z roku 1997. Evans ho pak zařadil do deváté kapitoly knihy Domain-Driven Design.

Ve svém pozdějším souhrnu DDD Reference ho už jako samostatný vzor nemá.

Kdy Specification použít a kdy ne

Kdy se specifikace vyplatí? Původní článek jmenuje tři použití. Selection vybírá objekty, které pravidlo splňují. Validation ověřuje, že se objekt hodí pro daný účel.

A construction to order popisuje objekt, který se podle popisu teprve sestaví.

V Symfony projektu se vyplatí hlavně tam, kde se pravidlo skládá v různých kombinacích. Dál tam, kde platí v doméně i v dotazu do databáze.

A tam, kde se pravidlo skládá až za běhu, třeba promo kód s podmínkami z administrace.

Každé pravidlo ale stojí novou třídu, soubor a test. Nevyplatí se proto u podmínky, která se objeví jednou a vejde se do jednoho ifu.

Nehodí se ani pro invariant agregátu. Ten zůstává v agregátu jako privátní metoda. A feature flag doménové pravidlo vůbec není.

Kdo vzor právě objevil, často udělá tohle. Tři třídy pro větší, menší a rovno, každá s jedním řádkem kódu.

Jména přestanou být doménová a z kódu už doménu nevyčtete. Doménové jméno má nést výsledná specifikace, tedy ta, kterou předáváte dál.

Obecné kousky uvnitř složeného pravidla přitom nevadí, Evans s Fowlerem sami ukazují specifikaci maximální teploty. Problém nastane, až se přes ně začne ptát doména.

Krátký odhad. Podmínka na částku nad tisíc korun, použitá na jediném místě. Vlastní třída, nebo if přímo v kódu?

If přímo v kódu. Jedna podmínka na jednom místě si třídu, soubor a test nezaslouží. Pojmenovat ji má smysl, až se začne opakovat nebo skládat.

Kombinátory and, or, not

Vzor se vyplatí hlavně díky skládání přes kombinátory and, or a not. Místo klubka ifů vznikne výraz nad pojmenovanými kousky.

Rozhraní se proto rozšíří o tři metody. Každá vrací zase specifikaci, takže se dají řetězit.

Implementaci dodá abstraktní třída CompositeSpecification. Každá metoda jen vyrobí odpovídající uzel stromu.

Uzel pro and drží dvě specifikace a ptá se obou. Kandidát projde, jen když souhlasí levá i pravá.

Or se liší jediným operátorem. Not drží jen jednu specifikaci a její odpověď otočí.

Konkrétní pravidla pak kombinátory nepíšou. Specifikace dopravy zdarma jen vymění rozhraní za abstraktního předka a and, or a not zdědí.

Stejně vzniknou další dvě pravidla kontextu Ordering. InEUCountry kontroluje zemi doručovací adresy a NotInBlacklist hlídá zákazníka.

Jedna poznámka k modelu. Kanonická objednávka doručovací adresu nenese. Kniha ji pro tuhle ukázku rozšiřuje o vlastnost shippingAddress.

A to assert na začátku zužuje typ jen pro statickou analýzu. S vypnutými asercemi se v produkci nespustí, kompozici hlídá PHPStan nebo Psalm.

Zpátky k marketingové akci z úvodu. Tři vnořené ify teď nahradí jeden výraz.

Limit tisíc korun je zapsaný jako sto tisíc haléřů, protože Money drží částku v celých číslech. Následuje and se zemí v EU a and s blacklistem.

Tohle pravidlo jen odpoví true, nebo false a objednávku nemění. Nulové dopravné pak dosadí handler checkoutu, který ji volá.

Teď zkuste odhadnout. Produktový tým zruší kontrolu blacklistu. Kolik řádků se změní?

Jeden. Z výrazu zmizí poslední and a nikdo nemusí luštit vnořený if. V testu navíc jde každý kousek ověřit zvlášť.

Specifikace jako dotaz do Doctrine

Specifikace má i druhou roli. Místo nové metody pro každou kombinaci pravidel dostane repozitář libovolnou specifikaci a vrátí objednávky, které ji splňují.

Práce se dělí jasně. Specifikace nese pravidlo a repozitář ví, jak ho přeložit do persistence. Stačí mu zavolat metodu toExpression a výsledek předat Doctrine.

Svádí to předat specifikaci QueryBuilder, ať si do něj podmínku dopíše. Jenže metoda, která nic nevrací, se skládat nedá. Or ani not pak nepřeložíte.

Proto specifikace vrací výraz Doctrine Criteria. Metoda toExpression postaví podmínku na měnu a na částku.

Bacha na pole ve výrazu. Criteria vidí jen mapované vlastnosti. Kanonická objednávka ale součet počítá z položek, takže dotaz potřebuje částku uloženou zvlášť.

Repozitář pak nabídne obecnou metodu match. Doctrine repozitář umí matching, protože implementuje rozhraní Selectable.

Kombinátory se přeloží stejně přímočaře. And na andX, or na orX a not na not.

Pravidlo teď existuje dvakrát, jednou v PHP a jednou jako dotaz. Obě podoby sedí v jedné třídě, jenže jestli spolu souhlasí, nic nehlídá.

Co se stane, když se ty dvě verze časem rozejdou?

Nic, čeho byste si hned všimli. Objednávka v PHP projde a dotaz ji nevrátí, nebo naopak. Pojistkou je kontraktní test, který nad stejnými daty porovná isSatisfiedBy s výsledkem match.

Criteria má i své limity. Joiny, agregace, poddotazy ani vlastní DQL funkce do výrazu nedostanete. Pak zůstává vlastní metoda repozitáře a s pravidlem ji sváže kontraktní test.

Hotovou kostru přitom nabízí balíček happyr doctrine specification. Mimo výukové ukázky vyjde levněji.

Otázky a shrnutí

Otázka na rozmyšlenou. Proč metoda toExpression vrací výraz a neupravuje rovnou QueryBuilder?

Metoda, která nic nevrací, se skládat nedá. Výraz Criteria jde vložit do andX, orX i not, takže se přeloží celá kompozice.

A jedna z dílu o hodnotových objektech. Proč Money drží tisíc korun jako sto tisíc haléřů v celém čísle?

Float by do peněz vnesl chyby zaokrouhlení a ty se projeví až na faktuře. Proto se částka drží v nejmenších jednotkách měny jako celé číslo.

Takže v kostce. Specifikace dá doménovému pravidlu jméno a vlastní typ.

Kombinátory and, or a not nahradí vnořené ify jedním výrazem. A přes Criteria jde totéž pravidlo i do databáze, soulad obou podob hlídá kontraktní test.

Příští díl je o doménových službách, továrnách a modulech. Celou kapitolu o doplňujících taktických vzorech najdete v knize na ddd-v-symfony.katuscak.cz.

← Všechny díly videokurzu