TDD pre DDD a DDD pre TDD

Zdeno Jašek

05.03.2019

Ako súvisí Test-driven development (TDD) s Domain-driven designom (DDD)? Na pohľad rôzne oblasti vývoja softvéru smerujúce do nesúvisiacich častí majú veľa spoločné a výborne sa dopĺňajú.

Domain-driven design

Hlavným cieľom Domain-driven designu je vytvoriť spoločný doménový jazyk pre používateľa, analytika, dizajnéra, programátora a testera. Dôležité je, aby týmto jazykom hovoril aj zdrojový kód, aby sa pojmy zo spoločného jazyka používali nielen v analytických materiáloch a na používateľských obrazovkách, ale aj v zdrojovom kóde. Na dosiahnutie tohto cieľa používa objektovo-orientovanú analýzu a dizajn. Jedinečnosť objektovo-orientovaného prístupu spočíva v tom, že dokáže spojiť pojmy z reálneho sveta s pojmami zo sveta analýzy a tie zase konzistentne napojiť na objekty v zdrojovom kóde. Objektovo-orientovaný prístup má kľúčovú vlastnosť, ktorá je pre unit testy veľmi dôležitá: zapúzdrenosť.

Test-driven development

Test-driven development je stratégia vývoja založená na písaní unit testov. Ak chce programátor implementovať napríklad procedúru na sčítanie dvoch čísel, v prvom kroku napíše najprv test, ktorý zisťuje, či procedúra funguje. Vzápätí zistí, že test mu hádže chybu: keďže procedúra ešte neexistuje, test sa nedá ani len skompilovať. V ďalšom kroku programátor implementuje čo najjednoduchším spôsobom procedúru tak, aby test zbehol – napr. „natvrdo“ nechá vrátiť tú správnu návratovú hodnotu. Potom píše ďalší test, ktorým sa procedúru snaží usvedčiť z nefungovania, aby ju následne opäť mohol opraviť. Tento prístup opakuje dokola: „chybný test = opravujem implementáciu“, „funkčný test = píšem nový test“. Vznikajú testy alternatívnych scenárov a rôznych hraničných situácií. A k tomu implementácia, ktorá všetkým testom vyhovuje.

A čo má TDD a DDD spoločné?

Dobrý dizajn.
Test-driven development sa veľmi ľahko a dobre vysvetľuje na jednoduchom príklade, s ktorým sa programátori v praxi nestretávajú (naprogramujte súčet dvoch čísel). Pri programovaní komplikovaných aplikácií je biznis logika obvykle závislá na rôznych technologických konceptoch: čítanie a zápis do databázy, čítanie XML súborov, volanie integračných služieb, posielanie e-mailu, ručný vstup používateľa apod. Napísať unit test na metódu v takto komplexnom prostredí je nemožné.


A práve v tomto bode sa TDD stretáva s DDD. TDD hovorí, že ak sa v programe ťažko píšu unit testy, tak má asi zlý dizajn. A DDD hovorí, ako by ten dobrý dizajn mal vyzerať.

Dobrý dizajn knihy jázd

Dajme to, že je potrebné v doméne „kniha jázd“ vypočítať priemernú spotrebu vozidla pre danú knihu jázd. Výpočet priemernej spotreby vozidla závisí od počtu najazdených kilometrov a celkového objemu natankovaných litrov. Treba aj zohľadniť počet litrov, ktoré malo vozidlo natankované na začiatku a na konci mesiaca:

Priemerná spotreba = ( litre na začiatku – litre na konci + natankované litre ) / počet kilometrov * 100

Vzorec je síce jednoduchý, ale na implementovanie metódy je nutné načítať všetky jazdy a všetky tankovania z databázy. Takisto je nutné načítať aj litre na začiatku a litre na konci, ktoré sú uložené v knihe jázd:

Takže algoritmus na výpočet priemernej spotreby robí toto:

  1. Načítaj z databázy Knihu jázd.
  2. Ku Knihe jázd dočítaj z databázy zoznam jázd.
  3. Sčítaj všetky atribúty „vzdialenosť“ a ulož ich do premennej „počet kilometrov“.
  4. Ku Knihe jázd dočítaj z databázy zoznam tankovaní.
  5. Sčítaj všetky atribúty „tankovanie v litroch“ a ulož ich do premennej „natakované litre“.
  6. Z objektu Kniha jázd načítaj atribúty „litre na začiatku“ a „litre na konci“.
  7. Zober všetky získané hodnoty a vypočítaj z nich priemernú spotrebu podľa uvedeného vzorca.

S ohľadom na architektúru aplikácie môže byť algoritmus implementovaný rôznymi spôsobmi: priamo ako REST-ová služba (napr. v microservices), alebo rozbitá na menšie časti podľa domény.

Algoritmus ako jedna služba

V tomto prípade sú všetky body algoritmu 1-7 implementované v rámci jednej služby (procedúry). Kvôli otestovaniu celého algoritmu je potrebné pripraviť aj samostatnú testovaciu databázu, do ktorej sa naplnia príslušné hodnoty, aby sa mohol otestovať správny výsledok.

Výhodou tohto prístupu je jeho priamočiarosť – samotný algoritmus je priamo v implementácii služby a teda aj implementácia prebieha bez ďalšej dekompozície.

Nevýhodou prístupu je nízka flexibilita kódu a ťažká testovateľnosť. Na otestovanie funkčnosti je nutné udržiavať samostatnú databázu. Náklady na údržbu takýchto testov sú pomerne vysoké. Testy majú tendenciu hlásiť chyby aj vtedy, keď v kóde žiadne chyby nie sú – a to kvôli vzájomnému ovplyvňovaniu dát v databáze. Veľmi často dochádza k tomu, že sa testy vytvorené počas vývoja prestanú spúšťať, lebo ich údržba je drahšia ako opravy chýb.

Nízka flexibilita kódu zase spočíva v tom, že pri malej zmene zadania je nutné novú požiadavku implementovať nanovo. Napríklad pri požiadavke „vypočítajte priemernú spotrebu vozidla za rok“ by úprava algoritmu bola síce jednoduchá, ale vytvorenie novej služby by programátor riešil zrejme cez copy-and-paste.

Objektová dekompozícia

Domain-driven design vedie k objektovej dekompozícii. V príklade je možné postupovať dvomi rôznymi spôsobmi:

  1. Využiť koncepciu „Repository“ z DDD a vyviesť mimo domény dotazy na databázu.
  2. Využiť koncepciu „Agregát“ z DDD a celý výpočet odvodiť od objektu „Kniha jázd“.

Repository

Repository je z pohľadu DDD rozhranie, ktoré je následne implementované mimo doménovej logiky. Vďaka koncepcii „Repository“ z DDD je možné z algoritmu vyčleniť pomocou rozhraní tie kroky, ktoré obsahujú volania databázy. Takto je možné priamo do triedy „Kniha jázd“ doplniť metódu „vypocitajPriemernuSpotrebu“ so vstupnými parametrami „JazdaRepository“ a „TankovanieRepository“. Samotné implementácie repository potom zabezpečia vykonanie dotazov nad databázou, takže body 2 a 4 z postup algoritmu budú implementované mimo domény.

V tomto prípade je možné napísať unit test priamo nad metódou „vypocitajPriemernuSpotrebu“ bez toho, aby musel programátor kvôli tomu udržiavať vlastnú databázu. Stačí implementovať rozhrania repository a samotnej metóde tak podstrčiť testovacie dáta. Príprava testovacích dát možno nebude úplne triviálna, ale tá by bola nutná aj v prípade použitia skutočnej relačnej databázy. Uvedený prístup je teda založený na predstave, že v aplikácii existuje technologická vrstva „application“, ktorá obsahuje technologické koncepty (napr. prístup k databáze) a doménová vrstva „domain“ so samotnou biznis logikou:

Výhodou postupu je teda lepšia testovateľnosť. Výhodou je aj to, že doménové metódy vznikajú priamo na doménových objektoch a dajú sa ľahšie znovupoužiť v rôznych kontextoch, t.j. na implementáciu rôznych služieb.

Nevýhodou je vyššia náročnosť implementácie. Namiesto priamočiareho prístupu k implementácii sa vyžaduje pridanie nových rozhraní, ktoré musia byť takisto implementované samostatnými triedami (mimo doménového modelu).

Agregát

Ďalšou možnosťou je definovať triedu „Kniha jázd“ ako agregát. Z pohľadu DDD to znamená, že jazdy a tankovania sú neoddeliteľnou súčasťou objektu „Kniha jázd“, t.j. že sa z databázy vždy načítavajú spolu a naraz.

Výhodou je isté zjednodušenie samotnej metódy. Doména sa zaobíde bez „JazdaRepository“ a „TankovanieRepository“ a teda aj metóda „vypocitajPriemernuSpotrebu“ nepotrebuje vstupné parametre. Z pohľadu testovateľnosti je situácia rovnaká ako pri príklade s repository.

Nevýhoda spočíva v manipulácii s agregátmi. Z popisu fungovania agregátu je zrejmé, že ak treba v aplikácii zobraziť napríklad 20 kníh jázd, znamená to, že nad databázou prebehne 41 dopytov. 1 dopyt načíta 20 kníh jázd a pre každú z nich sa spustia dva ďalšie dopyty (na jazdy a na tankovania). Tento dôsledok predstavuje ďalšiu komplikáciu a smeruje k oddeleniu čítania dát od manipulácie s nimi (CQRS).

Agregát je v DDD pomerne zložitý koncept a jeho použitie nevychádza len z toho, že niektorá z metód potrebuje navigáciu do zoznamu iných entít. Avšak v tejto doméne v rámci uvedených entít sa použitie agregátu javí ako oprávnené.

TDD a DDD – zhrnutie

Test-driven development vyžaduje, aby testy boli písané skôr ako implementácia. Táto požiadavka smeruje práve k tomu, aby programátor riešil dizajn skôr než pristúpi k implementácii. Domain-driven design zase obsahuje sadu návodov a filozofií, ako má dobrý dizajn vyzerať. Preto symbióza oboch taktík vo vývoji softvéru poskytuje dobrý synergický efekt.

Namiesto záveru: hlbší dizajn

V uvedenom príklade výpočtu priemernej spotreby ostal nevyriešený jeden problém spomenutý v texte: čo ak príde požiadavka na zobrazenie priemernej spotreby jedného vozidla za celý rok? Pre takúto požiadavku nie je navrhovaný dizajn vhodný. Najmä fakt, že metóda „vypocitajPriemernuSpotrebu“ sídli priamo v triede „Kniha jázd“ je v tomto smere dosť zväzujúci. Vzniká otázka, či je rozumné, aby trieda „Kniha jázd“ bola zodpovedná za výpočet priemernej spotreby.

Riešenie tohto problému už presahuje rámec tohto článku a teda nechám ho na cteného čitateľa.

Ďalšie blog posty, čo by ťa mohli zaujímať