Mnoho začínajících programátorů považuje pojmenovávání proměnných, funkcí, souborů a dalších komponent za nepříliš důležité. Výsledkem je, že jejich logika návrhu je často správná - algoritmy běží rychle a přinášejí požadovaný efekt, přičemž mohou být sotva čitelné. V tomto článku se pokusím stručně popsat, čím bychom se měli řídit při pojmenovávání různých prvků kódu a jak nezacházet z jednoho extrému do druhého.
Proč zanedbání fáze pojmenování prodlouží (v některých případech až enormně) vývoj vašeho projektu?
Předpokládejme, že vy a váš tým přebírají kód od ostatních programátorů. Stránky projekt jste zdědili, byla vytvořena bez jakékoliv lásky - fungovala dobře, ale každý její prvek mohl být napsán mnohem lépe.
Pokud jde o architekturu, v případě dědičnosti kódu to téměř vždy vyvolá nenávist a hněv programátorů, kteří ji dostali. Někdy je to způsobeno použitím vymírajících (nebo zaniklých) technologií, někdy špatným způsobem uvažování o aplikaci na začátku vývoje a někdy prostě jen nedostatkem znalostí odpovědného programátora.
V každém případě je možné, že s postupujícím časem projektu dojde k bodu, kdy programátoři zuří nad architekturami a technologiemi. Koneckonců, každá aplikace potřebuje po nějaké době přepsat některé části nebo jen změnit určité části - to je přirozené. Problémem, ze kterého programátorům zešediví vlasy, je však obtížné čtení a pochopení zděděného kódu.
Zejména v extrémních případech, kdy jsou proměnné pojmenovány pomocí jednotlivých nesmyslných písmen a funkce jsou náhlým přívalem kreativity, který není v žádném případě konzistentní se zbytkem aplikace, se mohou vaši programátoři zbláznit. V takovém případě si jakákoli analýza kódu, která by při správném pojmenování mohla proběhnout rychle a efektivně, vyžádá dodatečnou analýzu algoritmů zodpovědných například za vytvoření výsledku funkce. A taková analýza, byť nenápadná - ztrácí obrovské množství času.
Implementace nových funkcí v různých částech aplikace znamená projít noční můrou její analýzy, po nějaké době se musíte ke kódu vrátit a analyzovat ho znovu, protože jeho záměry nejsou jasné a předchozí čas strávený snahou pochopit jeho fungování byl zbytečný, protože si už nepamatujete, jaký byl jeho účel.
A tak jsme vtaženi do tornáda nepořádku, které v aplikaci vládne a pomalu pohlcuje každého účastníka jejího vývoje. Programátoři projekt nenávidí, projektoví manažeři neradi vysvětlují, proč se doba jeho vývoje začíná neustále prodlužovat, a klient ztrácí důvěru a zlobí se, protože nic nejde podle plánu.
Jak se tomu vyhnout?
Přiznejme si, že některé věci nelze přeskočit. Pokud jsme si na začátku projektu vybrali určité technologie, musíme si uvědomit, že časem buď přestanou být podporovány, nebo bude stále méně programátorů ovládat několik let staré technologie, které pomalu zastarávají. Některé knihovny při svých aktualizacích vyžadují více či méně náročné změny v kódu, což s sebou často nese vír závislostí, ve kterém můžete uvíznout ještě více.
Na druhou stranu to není tak černý scénář, technologie samozřejmě stárnou, ale faktorem, který rozhodně zpomaluje dobu vývoje projektů, které je využívají, je do značné míry ošklivý kód. A samozřejmě zde musíme zmínit knihu Roberta C. Martina - jedná se o bibli pro programátory, kde autor představuje spoustu osvědčených postupů a zásad, které by se měly dodržovat, aby bylo možné vytvářet kód, který usiluje o dokonalost.
Základem při pojmenovávání proměnných je jasně a jednoduše vyjádřit jejich záměr. Zní to docela jednoduše, ale někdy to mnoho lidí zanedbává nebo ignoruje. Dobrý název určí, co přesně má proměnná uchovávat nebo co má funkce dělat - nesmí být pojmenována příliš obecně, ale na druhou stranu se z ní nesmí stát dlouhý slint, jehož pouhé čtení způsobuje mozku nemalé problémy. Po nějaké době s dobrým kód kvality, zažíváme efekt ponoření, kdy jsme schopni podvědomě uspořádat pojmenování a předávání dat funkci tak, že celá věc nezanechává iluze o tom, jaký záměr ji řídí a jaký je očekávaný výsledek jejího volání.
Další věc, kterou lze nalézt v JavaScript, je mimo jiné snaha o nadměrnou optimalizaci kódu, která jej v mnoha případech činí nečitelným. Je normální, že některé algoritmy vyžadují zvláštní péči, což často odráží skutečnost, že záměr kódu může být poněkud spletitější. Nicméně případy, kdy potřebujeme nadměrnou optimalizaci, jsou extrémně vzácné, nebo alespoň ty, kdy je náš kód špinavý. Je důležité si uvědomit, že mnoho optimalizací souvisejících s jazykem probíhá na poněkud nižší úrovni abstrakce; například engine V8 dokáže při dostatečném počtu iterací výrazně zrychlit smyčky. Co je třeba zdůraznit, je skutečnost, že žijeme v 21. století a nepíšeme programy pro misi Apollo 13. V tomto případě se jedná o to, že se jedná o program, který je určen pro misi Apollo. V tématu prostředků máme mnohem větší manévrovací prostor - jsou tu od toho, aby se používaly (nejlépe rozumným způsobem :>).
Někdy rozdělení kódu na části opravdu hodně dá. Pokud operace tvoří řetězec, jehož účelem je provést akce zodpovědné za konkrétní modifikaci dat - je snadné se ztratit. Proto lze jednoduchým způsobem, místo abyste vše prováděli v jednom řetězci, rozdělit jednotlivé části kódu, které jsou zodpovědné za konkrétní věc, na jednotlivé prvky. To nejenže zpřehlední záměr jednotlivých operací, ale také vám to umožní testovat fragmenty kódu, které jsou zodpovědné pouze za jednu věc a lze je snadno opakovaně použít.
Několik praktických příkladů
Myslím, že nejpřesnějším vyjádřením některých výše uvedených tvrzení bude ukázat, jak fungují v praxi - v tomto odstavci se pokusím nastínit některé špatné postupy v kódu, které lze více či méně změnit na dobré. Upozorním na to, co v některých momentech narušuje čitelnost kódu a jak tomu zabránit.
Zhouba jednopísmenných proměnných
Hroznou praxí, která je bohužel poměrně běžná i na univerzitách, je pojmenovávání proměnných jedním písmenem. Nelze nesouhlasit s tím, že je to někdy docela pohodlné řešení - vyhneme se zbytečnému přemýšlení, jak určit účel proměnné, a místo toho, abychom k jejímu pojmenování použili několik či více znaků, použijeme jen jedno písmeno - např. i, j, k.
Paradoxně jsou některé definice těchto proměnných opatřeny mnohem delším komentářem, který určuje, co měl autor na mysli.
Dobrým příkladem by zde byla iterace nad dvourozměrným polem, které obsahuje odpovídající hodnoty v průsečíku sloupce a řádku.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
// dost špatné
for (let i = 0; i < array[i]; i++) {
for (let j = 0; j < array[i][j]; j++) {
// zde je obsah, ale pokaždé, když jsou použity i a j, musím se vrátit a analyzovat, k čemu jsou použity
}
}
// stále špatné, ale zábavné
let i; // řádek
let j; // sloupec
for (i = 0; i < array[i]; i++) {
for (j = 0; j < array[i][j]; j++) {
// zde je obsah, ale pokaždé, když jsou použity i a j, musím se vrátit a zkontrolovat komentáře, k čemu jsou použity
}
}
// mnohem lepší
const rowCount = array.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = array[rowIndex];
const columnCount = row.length;
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
const column = row[columnIndex];
// Má někdo pochybnosti o tom, co je co?
}
}
Záludná nadměrná optimalizace
Jednoho krásného dne jsem narazil na vysoce sofistikovaný kód napsaný jedním softwarový inženýr. Tento inženýr přišel na to, že odesílání uživatelských oprávnění ve formě řetězců specifikujících konkrétní akce lze výrazně optimalizovat pomocí několika triků na úrovni bitů.
Takové řešení by pravděpodobně bylo v pořádku, kdyby cílem byl Commodore 64, ale účelem tohoto kódu byla jednoduchá webová aplikace napsaná v JS. Nastal čas tuto zvláštnost překonat: Řekněme, že uživatel má v celém systému pouze čtyři možnosti úprav obsahu: vytvořit, číst, aktualizovat, odstranit. Je celkem přirozené, že tato oprávnění posíláme buď ve formě JSON jako klíče objektu se stavy, nebo jako pole.
Náš chytrý inženýr si však všiml, že číslo čtyři je v binárním zobrazení magickou hodnotou, a vyřešil to následovně:
Celá tabulka schopností má 16 řádků, uvedl jsem pouze 4, abych zprostředkoval představu o vytváření těchto oprávnění. Čtení oprávnění probíhá následovně:
To, co vidíte výše, není Kód WebAssembly. Nechci, abyste mě špatně pochopili - takové optimalizace jsou normální věcí pro systémy, kde určité věci potřebují zabírat velmi málo času nebo paměti (nebo obojí). Na druhou stranu webové aplikace rozhodně nejsou místem, kde by takovéto přehnané optimalizace dávaly úplný smysl. Nechci zobecňovat, ale v práci front-end vývojářů se složitější operace dosahující úrovně bitové abstrakce provádějí jen zřídka.
Je prostě nečitelný a programátor, který umí udělat analýzu takového kódu, se jistě podiví, jaké neviditelné výhody toto řešení má a co může být poškozeno, když se kód vývojový tým chce přepsat na rozumnější řešení.
A co víc - předpokládám, že odeslání oprávnění jako běžného objektu by programátorovi umožnilo přečíst záměr za 1-2 sekundy, zatímco analýza celé věci od začátku zabere nejméně několik minut. V projektu bude několik programátorů, každý z nich bude muset na tento kus kódu narazit - budou ho muset analyzovat několikrát, protože po nějaké době zapomenou, jaká kouzla se tam dějí. Stojí za to těch pár bajtů zachránit? Podle mého názoru ne.
Rozděl a panuj
Vývoj webových stránek rychle roste a nic nenasvědčuje tomu, že by se v tomto ohledu mělo brzy něco změnit. Musíme si přiznat, že v poslední době výrazně vzrostla odpovědnost front-end vývojářů - převzali část logiky zodpovědnou za prezentaci dat v uživatelském rozhraní.
Někdy je tato logika jednoduchá a objekty poskytované rozhraním API mají jednoduchou a čitelnou strukturu. Někdy však vyžadují různé typy mapování, třídění a dalších operací, aby se přizpůsobily různým místům na stránce. A to je místo, kde můžeme snadno spadnout do bažiny.
Mnohokrát jsem se přistihl při tom, že data v prováděných operacích jsou prakticky nečitelná. I přes správné použití metod pole a správné pojmenování proměnných se z řetězců operací v některých okamžicích téměř vytrácel kontext toho, čeho jsem chtěl dosáhnout. Také některé z těchto operací bylo někdy třeba použít jinde a někdy byly natolik globální nebo sofistikované, že vyžadovaly psaní testů.
Já vím, já vím - tohle není žádný triviální kus kódu, který by snadno ilustroval to, co chci sdělit. A také vím, že výpočetní složitost obou příkladů se mírně liší, přičemž v 99% případech si s tím nemusíme lámat hlavu. Rozdíl mezi algoritmy je jednoduchý, protože oba připravují mapu míst a vlastníků zařízení.
První z nich připravuje tuto mapu dvakrát, zatímco druhá pouze jednou. A nejjednodušší příklad, který ukazuje nás že druhý algoritmus je přenositelnější, spočívá v tom, že u prvního algoritmu musíme změnit logiku vytváření této mapy a např. provést vyloučení určitých míst nebo jiné podivnosti, kterým se říká obchodní logika. V případě druhého algoritmu měníme pouze způsob získání mapy, zatímco všechny ostatní úpravy dat probíhající v následujících řádcích zůstávají beze změny. V případě prvního algoritmu musíme upravit každý pokus o přípravu mapy.
A to je jen příklad - v praxi existuje spousta takových případů, kdy potřebujeme transformovat nebo refaktorovat určitý datový model celé aplikace.
Nejlepším způsobem, jak se vyhnout sledování různých obchodních změn, je připravit globální nástroje, které nám umožní získávat informace, které nás zajímají, poměrně obecným způsobem. I za cenu těch 2-3 milisekund, které bychom mohli ztratit na úkor deoptimalizace.
Souhrn
Programování je povolání jako každé jiné - každý den se učíme novým věcem a často děláme spoustu chyb. Nejdůležitější je se z těchto chyb poučit, stát se ve své profesi lepším a v budoucnu tyto chyby neopakovat. Nelze věřit mýtu, že práce, kterou děláme, bude vždy bezchybná. Můžete však na základě zkušeností ostatních nedostatky patřičně omezit.
Doufám, že vám přečtení tohoto článku pomůže vyhnout se alespoň některým z těchto problémů. špatné postupy kódování které jsem zažil při své práci. V případě jakýchkoli dotazů týkajících se osvědčených postupů při tvorbě kódu se můžete obrátit na Posádka The Codest ven, abyste se poradili o svých pochybnostech.