fbpx

Akú výhodu má zabaliť kolekciu dát do triedy?

V článku Aký test mám napísať som spomenul, že rád zabaľujem kolekcie údajov do vlastnej triedy. V tomto článku chcem vysvetliť prečo to preferujem pred priamym používaním listov a máp.

Čo je problém?

Iste ste už videli kade-tade po aplikácii roztrúsené takéto atribúty:

private List<Employee> employees;

alebo metódy s takýmito parametrami:

public class PayrollService {

	private List<Employee> employeesWithOvertime(List<Employee> employees, int monthlyHours) {
        return employees.stream()
			.filter(emp -> emp.getHours() > monthlyHours)
			.collect(Collectors.toList());
	}
}

Všimnime si, že tá metóda do toho servisu moc nepatrí. Áno, tam sa tá funkcionalita používa, ale patrí tam tá metóda? Odpoveď nám naznačí informácia, že všetko čo metóda robí sa deje s jej parametrami. Takže možno patrí do „triedy” jedného z jej parametrov. Ale takú triedu nemáme…

Odmyslime si, že je to hlúpy príklad (lebo je umelý a nechcem ho komplikovať). Čo keby sme vytvorili takúto triedu?

public class EmployeeCollection {
    private List<Employee> employees;

    public EmployeeCollection(List<Employee> employees) {
        this.employees = new ArrayList<>(employees);
    }

    public EmployeeCollection employeesWithOvertime(int monthlyHours) {
        List<Employee> filtered = employees.stream()
                .filter(emp -> emp.getHours() > monthlyHours)
                .collect(Collectors.toList());
        
        return new EmployeeCollection(filtered);
   	}
}

Aké to má výhody?

Najväčšie výhody, ktoré mi teraz napadli sú:

  • Ideálny kandidát na vývoj pomocou TDD

Keďže tento blog je zameraný na Test-Driven Development, tak začnem práve týmto bodom.

Dátové štruktúry sú zvyčajne najjednoduchšie testovateľné. Nevznikajú pri nich žiadne prekvapenia ani nežiadajú krkolomné mockovanie. Navyše je jednoduché pre ne definovať špecifikáciu a rovnako jednoduché je ju previesť do formy čitateľných testov. Nenájdete lepšiu príležitosť na vyskúšanie TDD ako pri vývoji dátovej štruktúry.

Po toľkých rokoch si to už presne nepamätám, ale myslím, že ku zabaľovaniu kolekcií som sa dostal práve cez TDD — chcel som si zjednodušiť moje TDD-čkovanie a hľadal som skratky, ako by to šlo ľahšie.

  • Je to objektovo orientované

Toto je ten najpodstatnejší dôvod, prečo to robím a vlastne všetko ostatné vyplýva z neho. Mám rád OOP a tým nemyslím, že si vytvorím niekoľko stateless tried ako Controller, Service, Repository, Mapper a k tomu pár tried pre údaje.

Čo v skutočnosti myslím je, že chápem zmysel a výhody tried, ktoré kombinujú stav a funkcionalitu, ktorá s tým stavom manipuluje.

V tomto prípade to je kolekcia EmployeeCollection, kde si ja definujem, ako sa s údajmi bude manipulovať, čo dokáže, či bude thread-safe a immutable. Namiesto toho, aby som to prenechal na iné triedy, ktoré tie údaje potrebujú používať.

  • Budúci domov pre ďalšie metódy

Môže sa vám zdať, že s tou kolekciou potrebujete spraviť len jednu alebo dve operácie, a teda vytvárať novú triedu je zbytočné. Hej, na začiatku môže mať len dve metódy. Ale postupne zistíte, že potrebujete spraviť aj iné operácie. Tie ľahko pridáte sem a vždy ich aj ľahko nájdete. Pretože sem prirodzene patria.

  • Kód sa neopakuje

Tento bod plynule naväzuje na predchádzajúci. Koľkokrát ste videli, že práca s kolekciou údajov je roztrúsená po niekoľkých servisoch alebo kontroleroch? Môže to byť len jednoduchá iterácia cez všetky prvky, filtrovanie podľa nejakých kritérií alebo validácia. Autor poslednej kópie ani nemusel byť spokojný, s tým, že robí ďalšiu kópiu operácie, ale jednoducho ju potreboval v inom servise, tak nemal ako zavolať metódu zo servisu, kde operácia už existovala.

Ešte horšia situácia je, keď operácie sú duplikované, ale časom sa prestanú na seba podobať. Sem-tam niektorá operácia robí niečo trošku iné. A teraz nastáva otázka — je to OK, alebo bug?

Kde sa neopakuje kód, nevznikajú ani chyby spôsobené duplikovaným kódom, prípadne sa opravené chyby neobjavujú neskôr znova v zabudnutej kópii.

Keď budú „všetky” operácie nad kolekciou údajov súčasťou tejto triedy, tak znížite množstvo kópií vo zvyšku kódu a zvýšite jeho čitateľnosť. Je predsa jednoduchšie pochopiť dobré meno metódy ako cyklus alebo prácu so stream().

  • Unit testy sa neopakujú a sú jednoduchšie

Keď máme kolekciu pokrytú kvalitnými unit testami, tak nemusíme klásť taký dôraz na použitie dát v unit testoch pre kontroler a servis. V každom nám stačí nám jeden test, aby sme mali istotu, že sa kolekcia používa a nemusíme „duplikovať” testy pre prázdnu, jednoprvkovú a viac-prvkovú kolekciu.

  • Optimalizácia je jednoduchšia

Nečakanú výhodu majú zabalené kolekcie pri optimalizovaní aplikácie.

Profiler, aplikácia na zisťovanie kde trávi aplikácia veľa času, zvyčajne netuší, že 3 % času strávené prácou v PayrollService a 2,5 %, čo ja viem, trebárs v PaymentProcessor je to isté. A je možné, že ani programátor si to nevšimne a jednotlivé časy sa mu budú zdať ako nepodstatné. Zato, keby videl, že 5,5 % času strávi aplikácia v EmployeeCollection, tak by možno spozornel.

  • Implementačné detaily sú skryté

Výhodou každej dobrej triedy je to, že skrýva svoje implementačné detaily pred okolím. A teda programátor sa môže rozhodnúť niečo v triede zmeniť. Či už zmeniť reprezentáciu údajov, zrýchlenie prístupu ku ním, alebo zavedenie kešovania. Toto sú techniky, ktoré by mohol použiť programátor z predchádzajúcej sekcie keď objaví problém s výkonom komponentu.

Ak si aplikácia všade posiela List<Employee>, tak sotva bude mať niekto chuť pohľadať všetky miesta a prekopať spôsob reprezentácie údajov. Zvyčajne to totiž znamená pomeniť celú reťaz volania metód a samozrejme aj prislúchajúce unit testy.

  • Zmeny pri nových požiadavkách sú jednoduchšie

Nové požiadavky na aplikáciu prichádzajú každý deň. Mnohokrát sú nepredvídané a podstatným spôsobom menia čo sa má diať.

Keď máte určitú funkcionalitu lokalizovanú na jednom mieste a dobre pokrytú testami, tak takéto zmeny nie sú strašiakom.

Príklad z mojej praxe — zabalená kolekcia hierarchických údajov potrebovala pridať operáciu vymazávania celého podstromu a neskôr dokonca presuny podstromov. Vďaka kvalitným testom to nebol žiaden problém — testy odchytili pár chýb, tie som opravil a všetko fungovalo ako malo bez problémov ďalej.

Záver

Ak niekedy pri svojej práci spozorujete, že potrebujete kolekciu dát, zastavte na chvíľu. Nie je toto vhodný okamih vyskúšať si niečo nové? Viete pre túto kolekciu nájsť vhodné doménové meno? Nevadí keď nie, ale s dobrým menom by ste pred kolegami ľahšie ukryli váš skutočný zámer, prečo ste práve tieto údaje zabalili do triedy — vyskúšať si TDD. (Stále medzi nami existujú ľudia, ktorým sa zdá, že čím menej tried a čím menej stavu v triedach, tým lepšie. Lebo pamäť počítača je predsa drahá a garbage collector je pomalý ?.)

Odmenou za vašu odvahu bude, že časom prídete na to, aká funkcionalita sa dá do kolekcie pridať. A časom aj na to, že zo začiatku umelá abstrakcia si začala žiť vlastným životom a je stále viac a viac užitočná.