V dnešním příspěvku se podíváme na to, proč je dobré psát kód na jedné úrovni abstrakce (angl. single level of abstraction). Ukážeme si několik příkladů kódu, který tomuto principu nevyhovuje, poté jej upravíme a uvidíme, jaké výhody nám to přineslo.
První příklad
Mějme kód v jazyce C++, který pracuje s kontakty na osoby uloženými v proměnné contacts
. Ta je typu std::map
a mapuje jméno osoby na seznam kontaktů, které na ni máme. Potřebujeme provést akci v závislosti na tom, zda máme kontakt na danou osobu. Typicky se to dá napsat takto:
if (contacts.find(name) != contacts.end) { // Kontakt byl nalezen. // ... } else { // Kontakt nebyl nalezen. // ... }
Když někdo tento kód uvidí, tak začne zkoumat význam kódu podmínky v příkazu if
a rozpozná v něm idiom jazyka pro zjištění, zda std::map
obsahuje daný klíč. Všimněte si ale, že se při čtení kódu musíte z jedné úrovně abstrakce (máme mapování jména na kontakty) přesunout na nižší úroveň abstrakce (jak se implementačně řeší zjištění, zda je v dané mapě daný klíč?). Vylepšení je jednoduché: vytvoříme šablonu hasKey(map, key)
, která vrátí bool
v závislosti na tom, zda daná std::map
obsahuje daný klíč:
if (hasKey(contacts, name)) { // ... } else { // ... }
Nyní se při čtení kódu nemusíte přesouvat na nižší úroveň abstrakce. Pokud by vás zajímalo, jak se implementačně zjistí, zda daná std::map
obsahuje daný klíč, tak mrknete na implementaci šablony hasKey()
. Tam uvidíte return map.find(key) != map.end()
.
Druhý příklad
Mějme funkci v jazyce Python, jejíž volání je výpočetně náročné a výstup závisí pouze na vstupních parametrech. Např. faktorizaci:
def factorize(num): ...
Po profilování zjistíte, že daná funkce je úzkým hrdlem a tak se rozhodnete výsledky cachovat (angl. memoization). Lze to s pomocí standardní knihovny (dekorátor functools.lru_cache
) udělat takto:
@functools.lru_cache(maxsize=None) def factorize(num): ...
Když však tento kód čtete, tak není na první pohled jasné, co se děje. Je tam nějaký dekorátor lru_cache
, kterému se nastaví maxsize=None
. Huh? Opět je zde problém s tím, že z jedné úrovně abstrakce (výsledky factorize() se mají cachovat) se musíme přesunout na nižší úroveň abstrakce (jak cachování implementovat). Vylepšení je opět nasnadě: vytvoříme si wrapper nad lru_cache()
, který bude mít zřejmější jméno a zapouzdří nám detaily:
@cache_results def factorize(num): ...
Pokud by vás to zajímalo, tak se onen wrapper dá napsat takto:
def cache_results(func): return functools.lru_cache(maxsize=None)(func)
či takto (kratší forma):
cache_results = functools.lru_cache(maxsize=None)
Co za výhody nám to přineslo?
Dodržením principu jedné úrovně abstrakce lze typicky získat následující výhody.
- Čitelnost. Nemusíme se při čtení kódu přepínat mezi úrovněmi abstrakce jen pro to, abychom zjistili interní fungování něčeho, co nás v danou chvíli nemusí zajímat.
- Znovupoužitelnost. Tím, že implementaci přesuneme do samostatné funkce, ji můžeme využít na více místech.
- Udržovatelnost. Co kdybychom např. potřebovali využít cachování u více funkcí? Rozkopírovat kód je špatné řešení. Když se např. rozhodneme pro cachování použít něco jiného, než
functools.lru_cache()
, tak při dodržení principu jedné úrovně abstrakce stačí změnit implementacicache_results()
místo toho, abychom vyhledávali a měnili všechny výskytylru_cache()
. - Korektnost. Čím více kódu napíšete, tím je větší pravděpodobnost, že v něm uděláte chybu. Při neustálém psaní
map.find(key) != map.end()
se může stát, že místo!=
omylem napíšete==
. Kód pak bude obsahovat chybu, které byste se snadno vyhnuli dodržením zmiňovaného principu. - Testovatelnost. Umístěním implementace do samostatných jednotek si usnadníme testování. Můžeme totiž vytvořené nízkoúrovňové funkce testovat v izolaci. U testování funkcí, které je používají se pak již nemusíme soustředit na všechny možné případy, které by mohly nastat, protože ony nízkoúrovňové funkce máme otestované.
Poznámky na závěr
Kdykoliv u kódu narazíte na případ, kdy se při jeho psaní musíte přesunout do jiné úrovně abstrakce (jak to naimplementovat?), myslete na to, že jeho umístěním do samostatné jednotky (funkce, třídy, modulu) můžete získat výhody zmíněné výše. Vůbec se nebojte toho, že pak budete mít více funkcí. Kód bude lepší.
Doporučuji dále mrknout na můj dřívější příspěvek nazvaný Tip pro lepší kód: místo kódu s komentářem napište funkci, který s principem jedné úrovně abstrakce úzce souvisí.