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.
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()
.
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)
Dodržením principu jedné úrovně abstrakce lze typicky získat následující výhody.
functools.lru_cache()
, tak při dodržení principu jedné úrovně abstrakce stačí změnit implementaci cache_results()
místo toho, abychom vyhledávali a měnili všechny výskyty lru_cache()
.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.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í.
Přidat komentář