Vývoj softvéru ako stavba babylonskej veže

Zdeno Jašek

22.08.2018
DDDProgramovanie

Podľa príbehu z Biblie babylonskú vežu ľudia nedostavali preto, lebo sa nemohli dohovoriť. Keď stavitelia hovoria rôznymi jazykmi, akýkoľvek komplikovaný projekt skončí nezdarom. Tento článok ukazuje, ako je možné postupovať pri implementácii domény tak, aby bola zachovaná hlavná myšlienka Domain Driven Designu: spoločný jazyk.

Hoci doslovný preklad pojmu „ubiquitous language“ je „všadeprítomný jazyk“, pojem „spoločný jazyk“ v slovenčine lepšie zodpovedá zamýšľanému významu.

Spoločný jazyk

Domain Driven Design je rozšírený prístup k navrhovaniu objektového modelu domény. Obvykle sa diskutujú jeho stavebné bloky ako „repository“, „factory“, „value object“, „aggregate“ atď. Všetky stavebné bloky však vychádzajú z jednej základnej myšlienky: „ubiquitous language“ – t.j. z existencie spoločného (všadeprítomného) jazyka. Spoločný jazyk je množina pojmov, ktoré používajú analytici, programátori, testeri, používatelia aplikácie aj doménoví experti, keď hovoria o doméne. Pričom pre všetkých má každý pojem rovnaký význam.

Technológie nie sú súčasťou domény

Pri písaní zdrojového kódu sa musia programátori vysporiadať s množstvom technologických konceptov, ktoré s doménou nesúvisia, ale pre beh programu sú nevyhnutné (databáza, prenos údajov po sieti, messaging, logovanie, bezpečnosť). Tieto koncepty je možné držať v samostatnej vrstve tak, aby do doménového modelu nezasahovali. Veľmi často sa využíva na tieto účely technika otočenia závislostí – „dependency inversion“.

Reprezentácia pojmov

Pri implementácii pojmov z domény je dôležité nájsť čo najvernejšiu reprezentáciu pojmu domény v analýze a v zdrojovom kóde. Objektovo-orientovaný prístup má v tomto smere výhodu v tesnej väzbe medzi analýzou a implementáciou. Ak analytik nájde správnu reprezentáciu pojmu pomocou tried, objektovo-orientovaný jazyk umožňuje túto reprezentáciu preniesť do zdrojového kódu. Analytik zodpovedá za to, aby špecifikovaná trieda čo najvernejšie odrážala doménu, t.j. chápanie pojmu používateľom. Programátor zase zodpovedá za to, aby implementácia triedy mala tesnú väzbu na analýzu.

Príklad: analýza rizík

V doméne „analýza rizík“ bude existovať pojem „Riziko“ s požiadavkou na výpočet miery rizika:

„Miera rizika sa počíta na základe pravdepodobnosti a dopadu podľa nasledujúcej tabuľky:“

Miera rizika Pravdepodobnosť Dopad
Vysoká Vysoká Veľký
Stredná Vysoká Malý
Nízka Nízka Veľký, Malý

Použitie novej technológie na projekte vývoja softvéru je typickým rizikom. Ak vývojársky tím ide prvýkrát použiť databázu „MongoDB“, projekt čelí riziku technologických problémov, ktoré má vysokú pravdepodobnosť a veľký dopad. A teda aj miera rizika je vysoká.

Analýza domény

Spoločný jazyk je v tomto diagrame zachovaný. Analytik narába s pojmami „Riziko“, „Pravdepodobnost“ a „Dopad“, pričom sa snaží pochopiť ich vzájomné vzťahy a závislosti.

Implementácia

public final class Riziko {
  private String nazov;
  private Pravdepodobnost pravdepodobnost;
  private Dopad dopad;
  
  public MieraRizika vypocetMieryRizika() {
    if ( pravdepodobnost == Pravdepodobnost.VYSOKA ) {
      return dopad == Dopad.MALY ? MieraRizika.NIZKA : MieraRizika.STREDNA;
    } else {
      return MieraRizika.NIZKA;
    }
  }
}

Spoločný jazyk je zachovaný aj v implementácii na úrovni pojmov. Ale metóda na výpočet miery rizika nepoužila spoločný jazyk, pretože použila algoritmický výpočet tam, kde bolo zadanie špecifikované deklaratívne vo forme tabuľky.

Táto implementácia má dva zásadné problémy:

  1. Zdrojový kód nezodpovedá zadaniu od analytika (hoci je implementovaný správne).
  2. Trieda Riziko má zodpovednosť za výpočet miery rizika.

Oba zásadné problémy sa naplno prejavia pri prvej požiadavke na zmenu.

Požiadavka na zmenu

„Malé rozšírenie tabuľky pre výpočet miery rizika:“

Miera rizika Pravdepodobnosť Dopad
Vysoká Vysoká Veľký
Stredná Stredná Veľký
Stredná Stredná Malý
Nízka Nízka Malý

 

V porovnaní s pôvodnou implementáciou navrhol teda používateľ zaviesť strednú pravdepodobnosť, ktorej zodpovedá aj stredná miera rizika.

Dopady na existujúci kód

Takáto požiadavka na zmenu má dva nepríjemné dopady na existujúci zdrojový kód:

  1. Vedie k modifikácii triedy Riziko, čo je v doméne „Analýza rizík“ zásadný pojem, na ktorý budú mať závislosti iné triedy. Akákoľvek zmena tejto triedy môže zasiahnuť všetky ostatné, ktoré na nej závisia. Dôsledkom je nutnosť komplexne pretestovať celý systém.
  2. Keďže pôvodná požiadavka nebola implementovaná v zmysle analýzy, bude potrebné celý algoritmus znovu premyslieť a prerobiť. V tomto príklade je algoritmus triviálny a teda ani jeho opätovné napísanie nebude náročné.

Refactoring, alebo ako to malo byť správne

Obom neželaným dopadom na existujúci kód sa dalo vyhnúť správnym návrhom modelu. Princíp jednoduchej zodpovednosti („single responsibility principle“) v tomto príklade hovorí, že trieda Riziko nemôže mať metódu vypocetMieryRizika, lebo riziko tým získava novú zodpovednosť: vie vypočítať mieru rizika. Správny model teda vyzerá takto:

Trieda VypocetMieryRizika je funkcia, ktorá zo vstupných parametrov Pravdepodobnost a Dopad vypočíta výsledok MieraRizika. V tomto modeli trieda Riziko vôbec nepozná pojem MieraRizika, pretože nie je zaťažená logikou ohľadom výpočtu.

Druhá zásadná úprava spočíva v zmene zdrojového kódu:

public enum VypocetMieryRizika {
  VYSOKA ( MieraRizika.VYSOKA, Pravdepodobnost.VYSOKA, Dopad.VELKY ),
  STREDNA ( MieraRizika.STREDNA, Pravdepodobnost.VYSOKA, Dopad.MALY ),
  NIZKA1 ( MieraRizika.NIZKA, Pravdepodobnost.NIZKA, Dopad.VELKY ),
  NIZKA2 ( MieraRizika.NIZKA, Pravdepodobnost.NIZKA, Dopad.MALY );
  
  private final MieraRizika miera;
  private final Pravdepodobnost pravdepodobnost;
  private final Dopad dopad;
  
  private VypocetMieryRizika( MieraRizika miera, Pravdepodobnost pravdepodobnost, Dopad dopad ) {
    this.miera = miera;
    this.pravdepodobnost = pravdepodobnost;
    this.dopad = dopad;
  }
  
  public static Optional<MieraRizika> vypocet(Pravdepodobnost pravdepodobnost, Dopad dopad ) {
    return Arrays.stream(   VypocetMieryRizika.values() )
    .filter( vypocet -> vypocet.dopad == dopad && vypocet.pravdepodobnost == pravdepodobnost )
    .map( vypocet -> vypocet.miera )
    .findAny();
  }

Výhodnou takto napísaného zdrojového kódu je presný obraz analytického zadania. Analytik sa s používateľom dohodol na tabuľkovom vyhodnocovaní výpočtu miery rizika, čiže použili deklaratívny prístup k analýze. Pre zachovanie spoločného jazyka je vhodné udržať deklaratívny prístup aj v implementácii. Kontrola kódu oproti zadaniu je veľmi jednoduchá. Cenou za to je však komplikovanejšia metóda „vypocet“. Prvá implementácia pomocou jedného príkazu „if“ pôsobí ďaleko jednoduchšie.

Výhodnou takto napísaného zdrojového kódu je presný obraz analytického zadania. Analytik sa s používateľom dohodol na tabuľkovom vyhodnocovaní výpočtu miery rizika, čiže použili deklaratívny prístup k analýze. Pre zachovanie spoločného jazyka je vhodné udržať deklaratívny prístup aj v implementácii. Kontrola kódu oproti zadaniu je veľmi jednoduchá. Cenou za to je však komplikovanejšia metóda „vypocet“. Prvá implementácia pomocou jedného príkazu „if“ pôsobí ďaleko jednoduchšie.

Požiadavka na zmenu ešte raz

Ako by vyzerala požiadavka na zmenu, ak by bol model navrhnutý správne?

V správnom modeli by vôbec nezasiahla triedu Riziko, čiže by nemala nečakané dopady na iné triedy, ktoré s objektami typu Riziko pracujú. Prípadné chyby v implementácii by mohli ovplyvniť spôsob výpočtu miery rizika, ale iné nechcené dôsledky by nastať nemohli.

A v druhom rade by verne kopírovala zmenu analytických podkladov:

VYSOKA (MieraRizika.VYSOKA, Pravdepodobnost.VYSOKA, Dopad.VELKY),
STREDNA1 (MieraRizika.STREDNA, Pravdepodobnost.STREDNA,Dopad.VELKY),
STREDNA2 (MieraRizika.STREDNA, Pravdepodobnost.STREDNA,Dopad.MALY),
NIZKA1 (MieraRizika.NIZKA, Pravdepodobnost.NIZKA, Dopad.VELKY),
NIZKA2 (MieraRizika.NIZKA, Pravdepodobnost.NIZKA, Dopad.MALY)

V uvedenej tabuľke chýba kombinácia pre vysokú pravdepodobnosť a malý dopad. Rovnakou chybou teda trpí aj zdrojový kód. Avšak chyby takéhoto typu sú ľahko komunikovateľné: keď si analytik, používateľ a programátor sadnú za spoločný stôl, budú mať v predstave rovnaký model a budú používať spoločný jazyk.

Desivý záver: nová požiadavka na zmenu

Namiesto tradičného happy endu končí tento článok desivou požiadavkou na zmenu. Pretože iné oddelenie sa rozhodlo úspešný softvér na analýzu rizík používať tiež.

  • Používateľ: „U nás sa miera rizika počíta veľmi jednoducho: pravdepodobnosť krát dopad.“
  • Analytik: „Krát?“
  • Používateľ: „Veď pravdepodobnosť je číslo od nula do jedna. A dopad je od 0 do 10.“

A úlohou analytika a programátora bude nájsť s používateľom opäť spoločný jazyk. Lebo veď stringy sa násobiť nedajú.

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