Defenzivní JavaScript? Rozhodně ano!

Každý vývojář zná ten nepříjemný pocit při hledání obskurního bugu. Programování by bylo rychlejší a příjemnější, kdybychom se těmto chybám mohli vyhnout. A tady nastupuje defenzivní programování.

V anglickém originále zveřejněno na blog.salsitasoft.com.

Každý opravdový vývojář zná ten nepříjemný pocit při hledání obskurního bugu. Programování by bylo mnohem rychlejší a především příjemnější, kdybychom se těmto chybám mohli jednoduchým způsobem vyhnout ještě předtím, než se vyskytnou. Jelikož se pravděpodobně v dohledné době žádného zázračného řešení nedočkáme, můžeme alespoň psát náš kód defenzivně, jak jen to bude možné. Co vlastně to defenzivní programování znamená? Wikipedie říká:

„Defensive programming is a form of defensive design intended to ensure the continuing function of a piece of software under unforeseen circumstances.“

(Ano, použili slovo „defenzivní“ v samotné definici.)

Jako javascriptový vývojář odkojený Javou jsem vážně přemýšlel, jak by šlo některé defenzivní techniky aplikovat ve světě JavaScriptu. JavaScript určitě není jazyk, kterému by tento styl seděl. Jedná se o dynamický jazyk, který interně neobsahuje typovou nebo syntaktickou kontrolu ve fázi kompilace. Pravděpodobně každý javascriptový vývojář již zažil problém spojený s přepisováním prototypu případně globální konflikty proměnných.

Není ale pravda, že JavaScript nelze psát defenzivně. Jen je potřeba vynaložit mnohem větší úsilí než v Javě nebo v jiném programovacím jazyku, který se snaží aktivně vývojáře chránit před jimi samotnými.

Tady je pár tipů pro javascriptové vývojáře, kteří chtějí minimalizovat riziko proniknutí těžko odhalitelných chyb do jejich kódů.

Používejte lint

Instalace lint pluginu do mého editoru měla dramatický efekt na kvalitu mého kódu. Každý snad zná JSLint, nástroj pro statickou analýzu JavaScriptu schopný odhalovat chyby a špatné praktiky, které jsou mnohem hůře odhalitelné při běhu aplikace. Ponechání tohoto pluginu na pozadí vašeho editoru vám umožní minimalizovat nespočet problému dříve, než se vůbec vyskytnou.

I přesto, že máte JSLint jako krok ve vašem build procesu (což by jste určitě mít měli), neexistuje náhrada pro odezvu v reálném čase přímo ve vašem editoru. Pokud to váš editor nepodporuje, tak pravděpodobně používáte špatný.

Zapouzdření vždy a všude

Zapouzdření znemožní (zne)užití proměnných na nesprávných místech. JavaScript implicitně nepodporuje privátní metody, můžeme ovšem využít scope funkce k dosažení podobného efektu. Jak jistě víte, když deklarujete novou proměnnou bez klíčového slova var (JSLint si rozhodně postěžuje), tato proměnná se vytvoří v globálním scope, a proto může způsobit konflikty s existujícími proměnnými nebo kódem třetích stran.

To je důvod, proč byste měli používat klíčové slovo var při deklaraci každé proměnné k dosažení správného zascopování k funkci, kde je proměnná použita. Tohoto faktu můžete využít a dosáhnout jednoduchého zapouzdření za pomocí skvěle pojmenovaných immediately-invoked function expression nebo taktéž IIFE. Tato technika by měla být známá Node.js vývojářům.

var Foo = function() {
    var privateBar = 0; // Toto nemůže být modifikováno mimo funkci Foo

    this.incrementPrivateVar = function() {
        privateBar++;
    };

    this.printPrivateVar = function() {
        console.log(privateBar);
    };
};

// Využijeme IIFE k tomu, aby veškeré deklarace proměnných byly viditelné
// pouze uvnitř tohoto bloku
(function() {
    var innerState;

    // Tato funkce nemůže být použita nebo změněna mimo IIFE
    var changeName = function(name) {
        // perform some decoration of inner state
        innerState = 'Stranger ' + name;
    };

    Foo.prototype.joinSession = function(name) {
        changeName(name);
        console.log('Hello ' + innerState);
    };

    Foo.prototype.secondPublicMethod = function() {
        console.log('I dare you to override me.');
    };
})();

// Nestane se nic, protože je funkce deklarovaná uvnitř IIFE
changeName = null;

// Jelikož je funkce přiřazená prototypu, cokoliv zvenku jí může přepsat
Foo.prototype.secondPublicMethod = function() {
    console.log('Oh no! You have been hacked.');
};

var foo = new Foo,
    bar = new Foo;

// Správně jsme použili scoping a proto je použita původní `changeName` metoda.
foo.joinSession('John Doe');
// Uuups, toto bylo přepsáno
foo.secondPublicMethod();
foo.incrementPrivateVar();
// vrací 1
foo.printPrivateVar();
// vrací 0 jelikož privátní proměnná je svázaná s konkrétní instancí
bar.printPrivateVar();

Organizujte váš kód v malých modulech obalených IIFE a vyhnete se spoustě drobných chyb.

Nechť jsou proměnné konstantní!

Moment… Konstantní? Proměnné? Tohle zní jako oxymóron? Ano, ale v reálném světě je spousta případů (možná dokonce většina), kdy „proměnná“ nepotřebuje měnit svou výchozí hodnotu.

Scala má skvělý koncept val (neměnná hodnota) a var (měnitelná proměnná). Čistě funkcionální jazyky jako např. Haskell nebo Clojure jsou v tomto ohledu ještě přísnější, poskytují pouze neměnné hodnoty a vyžadují speciální mechanismy k dosažení jejich měnitelnosti.

JavaScript obsahuje klíčové slovo const k vytvoření neměnných referencí, naneštěstí podpora napříč prohlížeči je stále vzácná. Používejte const pokud můžete, pokud ne, zamyslete se dvakrát před změnou hodnoty existující proměnné.

var foo = function(operand1, operand2, callback) {
    var sum = operand1 + operand2;

    // ... spousta kódu

    // Špatně!
    // Nový člen týmu chce použít proměnnou sum,
    // zpracovat jí a předat callbacku,
    // Ale moment! Opravdu musí modifikovat původní hodnotu?
    // Toto může potají zanést chybu při dalším zpracování v této funkci
    sum += 100;
    callback(sum);

    // Lépe!
    // Vykopírujte hodnotu do nové proměnné namísto změny původní proměnné.
    var mySuperSum = sum + 100;
    callback(mySuperSum);

    // ... spousta kódu

    return sum;
};

Neměnnost je důležitá

Očividně opravdu existují případy, kdy je nutné používat proměnné. Řekněme, že potřebujeme inkrementovat datum. Zvažte tento nepěkný kus kódu:

var startDate = new Date();

for (var i = 0; i < 7; i++) {
    startDate = new Date(startDate.getTime() + 86400000);
    console.log(startDate);
}

Určitě naleznete spoustu anti-patternu. Většina zkušených vývojářů upřednostňuje použití obalující knihovny namísto interní třídy Date, jelikož to zjednodušuje základní operace jako například přičítání nebo odečítání datumů. S tímto faktem nyní můžeme náš kód přepsat za použití Moment.js:

var startDate = moment();
for (var i = 0; i < 7; i++) {
    startDate.add(1, 'days');
    console.log(startDate);
};

Nyní zvažme název proměnné startDate.

Phil Karlton říká:

„There are only two hard things in Computer Science: cache invalidation and naming things.“

Používání proměnných činí rozhodnutí o jejich názvu složitější, jelikož širší doména možných hodnot proměnných způsobuje popis toho k čemu proměnná doopravdy slouží ještě méně zřejmé.

Hlavním problém kódu tedy tkví v měnitelnosti instance času. V softwarovém inženýrství je běžné používat modely, které korespondují s entitami z reálného světa. V tomto případě hraje instance času roli časového razítka a časové razítko je v reálném čase neměnné. Krom těchto konceptuálních úvah je taktéž snažší přehlédnout chybu při používání měnitelných instancí času.

var today = moment(),
    tomorrow = moment().add(1, 'days');

Zde je klasická ukázka, jak do kódu zanést velmi snadno přehlédnutelný bug, který vám jednoho dne přinese nespočet hodin zábavy strávené debuggingem. Na GitHubu zcela jistě naleznete velké množství repozitářů, které tuto konstrukci používají. V případě vykonání kódu přesně o půlnoci, tak, že první řádek se vykoná právě před půlnocí a řádek druhý přesně po půlnoci, rozdíl mezi těmito datumy nebude jeden den, ale dny dva.

„Šance, že se něco takového vyskytne, je tak malá,“ můžete namítat. „Proč se tímto vůbec zabývat?“. Tohle je přesně příklad, o čem je defenzivní JavaScript. Kdyby byl kód použit např. v Node.js, službě zpracovávající události přicházející z platební brány, zdánlivě zanedbatelná chyba by mohla mít obrovské následky.

var today = moment(),
    tomorrow = today.clone().add(1, 'days');

Lepší, stále je ovšem možné zanést chybu při zapomenutém clone. Člověk by si mohl myslet, že Moment.js není dostatečně defenzivní. Kdyby metoda add neměnila vnitřní stav původní instance, mohli bychom napsat něco podobného:

// Lepší, ale naneštěstí je způsobena změna vnitřního stavu proměnné today
var today = moment(),
    tomorrow = today.add(1, 'days');

Spojování řetězců

Při procházení code review jsem se nesčetněkrát setkal s podobným kódem:

  el.text(a + b);

Zdaleka není jasné, co se tu děje. Pro začátek by stálo za námahu vybrat srozumitelné názvy proměnných. Ale i kdyby byly rozumně pojmenovány (např. jméno a příjmení), pořád by mohly obsahovat neočekávaný datový typ, jako třeba integer. Chtěl autor v takovém případě tyto čísla zřetězit, nebo je sečíst?

Další problém je, že hodnota proměnné může být null nebo undefined. Jelikož se tyto hodnoty převádí na stringy „null“ a „undefined“, je v následujícím kódu ješte co zlepšovat.

name.text(title + ' ' + firstName + ' ' + lastName);

Z tohoto důvodu důrazně odrazuji od používání plus operátoru pro spojování řetězců (a String.prototype.concat není o nic lepší). Místo toho použijte knihovnu s kvalitním API pro formátování řetězců nebo ES6 template stringy.

Kde je if, tam je i else

Kolikrát jste napsali něco takového?

var foo = 'Default value';
...
if (condition) {
  foo = 'bar';
}

Považuji tento kód za těžko čitelný, protože to, co je konceptuálně else větev, je simulováno výchozí hodnotou. Nejedná se o velký problém v krátkém kódu, ale představme si, co se stane, když kód časem nevyhnutelně roste. Navíc se takto vzdáváme neměnnosti v případě, že je podmínka splněna. Je lepší napsat else větev explicitně:

var foo;
...
if (condition) {
  foo = 'bar';
} else {
  foo = 'Default value';
}

Obalujte bloky kódu závorkami. Vždy.

Proč obalovat jeden řádek kódu složenými závorkami? Je to přece škaredé. Jenže toto není věc vkusu. Závorky jsou nezbytné, pokud chcete programovat defenzivně.

if (condition)
    foo();
bar();

Vypadá to dost nevinně, ale v réalném světě by se vám mohlo podařit zakomentovat foo() při hledání nějakého bugu. Obalení bloku kódu do závorek navíc zjednodušuje refaktoring a minimalizuje změny (takže jsou diffy lépe čitelné) při přidávání nových řádků do bloku.

Pouze paranoidní (a defenzivní) přežijí

Abych to vše shrnul: Programování je obecně o práci v týmu. Pišme kód, jako bychom očekávali, že ho někdo rozbije. A tím minimalizujeme riziko, že se tak skutečně stane.

Salsita Software

Salsita Software je softwarová společnost, která se specializuje na vývoj komplexních moderních webových a mobilních aplikací. Sponzorujeme JavaScripting.com, komunitní portál, který pomáhá vývojářům hledat knihovny a frameworky pro JavaScript.

Autor: Tomáš Weiss
Přeložil: Luboš Turek a Tomáš Weiss

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 21

Přehled komentářů

HonzaMarek hm
Ondřej Novák Re: hm
HonzaMarek Re: hm
Ondřej Novák Re: hm
Ondřej Žára Re: hm
Ondřej Novák Re: hm
Ondřej Žára Re: hm
Ondřej Novák Re: hm
Ondřej Žára Re: hm
Ondřej Novák Re: hm
Ondřej Žára Re: hm
caracho if / else
Honza Práce v týmu
NoxArt Re: Práce v týmu
Honza Re: Práce v týmu
NoxArt Re: Práce v týmu
vm ESLint
Aleš Hájek Místo obskurně znásilňovaného Javascriptu,
Aleš Hájek A není nic horšího než udělat toto
tomkisw Re: A není nic horšího než udělat toto
Ondřej Žára Re: hm
Zdroj: https://www.zdrojak.cz/?p=15306