Navrhujeme triedy: Rodné čísla II.: Naspäť k tabuli

V jeden fiktívny deň vo fiktívnej firme prišiel k fiktívnemu vývojárovi fiktívny zákazník a riekol mu úplne nefiktívne a natvrdo:

Feri, chcem mať systém na evidenciu rodných čísiel.

Feri riekol:

“Huh?”

To bolo kedysi. V minulom dieli sme totiž strávili dostatok času na to, aby Feri nebol až taký zmätený.

Skončili sme pri návrhu triedy RodnéČíslo, ktorá vyzerala nasledovne:

class RodneCislo {
    boolean jeValidne()
    Date dajDatumNarodenia()
    boolean jeMuž()
    String toString()
    String toStringBezLomky()
}

Ešte raz: toto nie je zápis v žiadnom programovacom jazyku: je to prinajlepšom nejaká paJava.

Napriek tomu už teraz máme aspoň o niečo jasnejšiu predstavu, ako sa táto trieda bude používať. Najlepšie je napísať rovno kód v Jave.

Testy

RodneCislo rodneCislo = ???ČO TU MÁM NAPÍSAŤ?!!

Och, zhavarovali sme už na prvom riadku.

Ako máme vytvárať inštanciu rodného čísla? Žeby takto?

RodneCislo rodneCislo = new RodneCislo("721215/8231")

To znie celkom dobre. Z toho vyplýva, že potrebujeme do návrhu triedy zaviesť aj konštruktor:

RodneCislo(String retazecRodnehoCisla);

Pokračujme v ukážke kódu:

RodneCislo rodneCislo = new RodneCislo("721215/8231");
if(rodneCislo.jeValidne()) {
    String narodeny = rodneCislo.jeMuz() ? "Narodený" : "Narodená";
    System.out.println("Rodné číslo " 
        + rodneCislo.toString()
        + " " + narodeny 
        + " " + rodneCislo.dajDatumNarodenia());
} else {
    System.out.println("Nevalidné: " + rodneCislo.toString());
}

V tomto prípade sme ukázali použitie takmer všetkých metód — a to bez toho, aby sme mali dokončený kód triedy! To však vôbec neprekáža: ba priam je to výhodou, pretože ukazuje, či sa navrhnutá trieda bude dať rozumne používať.

Extrémne programovanie (filozofia programovania, nemýliť si s extrémistickými programátormi) zastáva zásadu

Najprv napíšte testy!

Pri návrhu triedy treba najprv napísať jej testy spolu s hlavičkami metód a až potom vypĺňať kód v metódach. Prakticky je to veľmi podobné (i keď viac rozpracované) tomu čo sme tu práve demonštrovali. Do detailov zatiaľ nechoďme, lebo nechceme dvojdielnu odbočku seriálu smerom k unit testom.

Diery v návrhu

Naše skvelo navrhnuté metódy majú v dvoch prípadoch jednu dieru. Filmoví fanúšikovia poznajú “plot hole”, teda dieru či nelogickosť v scenári, a my tu máme veľmi podobnú záležitosť.

Jedna súvisí s metódou dajDatumNarodenia() a ukáže sa v tomto teste:

RodneCislo rodneCislo = new RodneCislo("999999/9999");
Date narodeny = rodneCislo.dajDatumNarodenia();

Čo má obsahovať objekt narodeny? Deväťdesiatydeviaty štyridsiatehodeviaty (lebo je to ženské rodné číslo…) 1999?

Trieda rodných čísiel je zatiaľ skonštruovaná tak, aby vedela pojať aj nekorektné reťazce. Používateľ tejto triedy si má sám overiť, či je rodné číslo korektné (cez jeValidne()) a ak nie…

…tak sme to nedomysleli.

Problém spočíva v tom, že pre rodné číslo v nesprávnom formáte (napr. pre samé deviatky) chcipnu dve metódy:

  • jeMuž() bude zmätené, lebo rodné číslo neobsahuje na tretej pozícii ani nulu, ani jednotku.
  • dajDátumNarodenia() je nezmyselné, čo sme videli pred chvíľou.

To sú dve z piatich metód, čo nie je teda ktoviečo. (Ak zoberieme, že metóda pre tlač reťazca sú takmer analogické, zistíme, že polovica metód triedy je na nič.)

A to sme ešte nevideli príklad:

RodneCislo rodneCislo = new RodneCislo("LOL RODNE CISLO!")

Čo má znamenať toto? Náš dizajn sa rúca, čo znamená, že ho treba premyslieť. Naspäť k tabuli!

Dizajn znovu

Otázka, ktorá znie je:

Má zmysel mať objekt, ktorý sa nedá používať, lebo je v divnom stave?

Objekt v divnom stave?

Niekedy je dobré nájsť inšpiráciu v iných triedach projektov, na ktorých sa podieľali “skutoční dizajnéri(tm)”.

Vezmime si napr. java.lang.Long. Tá má konštruktor

public Long(String s) throws NumberFormatException

Po slovensky: konštruktor vie spracovať reťazec obsahujúci longové číslo, ale len vtedy, ak je v správnom formáte. Detaily poskytne dokumentácia (blabla, reťazec obsahuje číselnú reprezentáciu v desiatkovej sústave, blabla, na začiatku je voliteľný znak “mínus” atď). Nesprávny vstup spôsobí vyvolanie výnimky java.lang.NumberFormatException. To predchádza vytvoreniu nezmyselného Longu, čo je podobná situácia ako v prípade nášho rodného čísla.

Urobme teda podobný konštruktor v triede RodneCislo:

public RodneCislo(String retazecSRodnymCislom) throws ????

Akú výnimku má hádzať konštruktor? Buď si navrhneme vlastnú výnimku, alebo prebrúzdame dokumentáciu a využijeme povedzme java.text.ParseException, ktorá má vhodný názov (popisuje výnimku pri parsovaní vstupu) a je priamo zabudovaná do základnej knižnice Javy.

public RodneCislo(String retazecSRodnymCislom) throws ParseException

Ak vás poburuje príliš dlhý názov parametra, nezabúdajte na to, že v kóde metódy môžete používať autocomplete, teda automatické dopĺňanie. Rozhodne je to popisnejší názov ako napr. s, o ktorom sa môžete domnievať, čo má znamenať.

Keď už dumeme nad konštruktorom: aké typy vstupov vlastne chceme podporovať? Inak povedané, v ktorých prípadoch vstup prijmeme a kedy vyhodíme výnimku?

Totálne hlúposti (“debility”, o ktorých hovoril zákazník) vieme určiť hneď. Asi jedinou dilemou bude, či konštruktor zožerie aj vstupy s lomkou, aj bez lomky.

RodneCislo rodneCisloFera = new RodneCislo("7212158231");
RodneCislo toIsteRodneCisloFera = new RodneCislo("721215/8231");

Musíme sa rozhodnúť.

Nech sa už rozhodneme akokoľvek, je dobré takéto správanie zadokumentovať. I takáto drobnosť totiž má vplyv na to, či metóda triedy dokáže spraviť to, čo chceme. (Neskôr si povieme o tom, že všetky tieto úvahy definujú kontrakt medzi triedou a jej používateľom.)

Touto zmenou sme zabili dve muchy jednou ranou: nielenže nebudeme môcť vytvoriť nekorektné rodné číslo, ale zároveň sa vieme zbaviť jednej nadbytočnej metódy.

Áno, ide o

boolean jeValidne()

Keďže objekt rodného čísla bude vždy validný, metóda už nemá zmysel a môžeme ju vyhodiť.

Validita ešte raz

Toto zabíjanie múch a metód je chvályhodné: akurát v tejto chvíli používateľ triedy má len jednu možnosť, ako zistiť, či je rodné číslo správne: pokúsi sa vytvoriť jeho inštanciu a ak konštruktor vyhodí výnimku, znamená to, že niečo je zlé.

Možno by sa hodila jedna pomocná metóda, ktorá dostane na vstup reťazec a vráti buď true alebo false a to podľa toho, či je reťazec správny alebo nie. Táto metóda však nemôže byť konštruktorom: tie totiž nemôžu vracať booleovské hodnoty. Nemôže to byť ani metóda objektu: povedali sme si totiž, že nekorektný objekt rodného čísla nemožno vytvoriť.

Mohla by to však byť… statická metóda triedy RodneCislo.

Kým v bežných prípadoch máme nejaký hmatateľný objekt, na ktorý pokrikujeme rozkazy (dunčo.trhaj(), ofélia.choďDoKláštora()), statické metódy (alias metódy triedy; class methods), statické metódy sú “rozkazy”, ktoré nie sú smerované na objekt, ale na jeho triedu, mnohokrát preto, že na splnenie požadovaného rozkazu ani nepotrebujeme konkrétny objekt.

Statické metódy obvykle povážlivé: smrdia procedurálnym programovaním a la Pascal či C,, a kde je jedna, tam neraz vznikne druhá či piata. V tomto prípade je to však vhodné miesto, kde ju môžeme použiť.

Na overenie korektnosti reťazca s rodným číslom nepotrebujeme inštanciu. Stačí povedať:

Ó, ty predloha rodných čísiel, ty šablóna ľudských identifikátorov, povedz mi, či 7212158231 je korektné rodné číslo!

V rečí kódu:

boolean korektne = RodneCislo.jeKorektne("7212158231");

Všimnime si, že stále nič nehovoríme o tom, ako sa to dosiahne!

V Jave tomu bude zodpovedať metóda

public static boolean jeKorektne(String retazecSRodnymCislom)

Sumár triedy

Po upratovaní vyzerá patrieda takto:

class RodneCislo {
    /* konštruktor */
    RodneCislo(String retazecSRodnymCislom) throws ParseException

    Date dajDatumNarodenia()
    boolean jeMuž()
    String toString()
    String toStringBezLomky()

    boolean static jeValidne(String retazecSRodnymCislom)
}

Teraz je čas povedať si ako konkrétne budú jednotlivé metódy fungovať, presnejšie, ako bude vyzerať ich zdrojový kód.

Ale o tom nabudúce!