Přejít k navigační liště

Zdroják » JavaScript » Defenzivní JavaScript? Rozhodně ano!

Defenzivní JavaScript? Rozhodně ano!

Články JavaScript

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í.

Nálepky:

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

Komentáře

Subscribe
Upozornit na
guest
21 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
HonzaMarek

Simulovat privátní proměnný ve „třídách“ a kvůli tomu nepoužívat prototype pro metody není z výkonnostního hlediska úplně dobrý nápad.

Příklad s časama je zajímavej. Autor si pochvaluje jak se mu nemění reference na proměnnou, ale vesele mění samotnou hodnotu té proměnné. Přitom z toho, že věci jako čas nejsou immutable, taky vzniká hodně chyb.

Nepoužíval bych jedno klíčové slovo var pro více proměnných. Pak se dá snadno zapomenout čárka na konci a neštěstí je na světě. I když lint to pak zachrání.

Nepodoporu klíčového slova const řeší babel.

Odstranění else často naopak kód zjednoduší. Místo if (a) { return 1; } else if (b) { return 2; } else { return 3; } můžu napsat if (a) { return 1; } if (b) { return 2; } return 3;

Ondřej Novák

Jaké výkonostní problémy tam vidíte? Že se musí pokaždé v konstruktoru znova a znova inicializovat member promenne a funkce? Takže místo toho, aby je pak při volání JS hledal v prototypech je může vytáhnout rovnou z objektu.

HonzaMarek

Určitě je zbytečný ty metody při každém new vytvářet. Mohly by být definovány předem jako funkce a v konstruktoru by pak bylo jen přiřazení. Tím by se ušetřil čas i paměť.

Víc mi ale vadí, že to není „normální“ javascript, ale ohnuté řešení. Běžně se pro třídy používá prototyp s tím, že v JS nic jako privátní proměnné neexistuje a musí se s tím počítat. Jakékoliv neobvyklé řešení podle mě přináší jen problémy, když třeba má přijít nový programátor a podobně.

Ondřej Novák

Tak sorry, já myslel, že chytrý JS parser ty metody má už dávno vytvořený během parsování kódu, že tedy si je někam uloží do nějakých skrytých proměnných. Asi jsem moc napřed.

Jinak mi na prototypovém programování vadí roztříštěnost kódu, Je přehlednější, když věci spolu související jsou u sebe.

Navíc častokrát deklaruju i „privátní“ funkce a to i uvnitř jiných funkcí, třeba pokud nějaký callback potřebuju použít víckrát a přitom má smysl, aby byl vidět jen uvnitř té funkce a ne nikde vně.

Podle mě zakmnout se jen v nějakém patternu, který omezuje možnosti JS na minimum aby tomu rozuměli, …třeba javisté…, není dobrý. Schopný JS programátor by se měl vyznat i v čistě funkcionálním použíti JS… a samozřejmě v closure.

Ondřej Žára

Chytrý parser je — a bude — pořád jenom parserem. Těžko bude nějaký kód vykonávat, jako například cokoliv, co najde v konstruktoru. Co kdyby tam bylo nějaké volání? Co kdyby se tam dělo něco podmíněně? Co kdyby se tam ty metody — z nichž každá zabírá novou paměť, pro každou vytvořenou instanci! — vytvářely parametrizovaně, v závislosti na výsledku volání něčeho jiného v tom konstruktoru?

Navíc tedy, mějme kód

var F = function() {
  this.m = function() {}
}
var f1 = new F();
var f2 = new F();

V tomto případě je f1.m != f2.m, jsou to zcela odlišné funkce (mají akorát shodný zdrojový kód). Těžko by chytrý parser dopředu odvodil, kolik jich má vytvořit, že…

Zatímco u F.protoype.m budou obě ty samé, sdílené, vytvořené ještě před instancializací. Samosebou je výhoda tohoto řešení znát jen pokud vytvoříme >1 instanci :-)

Ondřej Novák

No z mého pohledu this.m je pořád tatáž funkce, jen při jejím spuštění se nasetuje jiný kontext. Nepsal bych to bezúčelně, nějaký jazyk s podobnou syntaxi a logikou programování jako je Javascript jsem sám napsal (aktuálně je SF mrtvý, zdrojáky tam budou, až se SF zmátoří). Nemůže se samozřejmě rovnat s možností JS, ale to taky nebyl účel. No a právě něco takového jsem tam řešil. Kompilátor vidí function tak přeloží její obsah do exekučního stromu a jeho root strčí jako konstantu. Inicializace objektu pak už není nic jiného, než pouhé nastavení proměnné na konstantu.

Funkce v JS mají navíc svůj kontext (closure) kde vznikly. Ale nemyslím si vůbec, že by každá funkce byla originál. Jestli to tak běžně parsery dělají… myslím že nedělají, minimálně V8 každou funkci přeloží JITem do strojového kódu v době parsování kódu, nepouští už JIT při každé instanci funkce. Maximálně bych připustil, že by nějak z té instanciace dokázal vytěžit a přeložit to lépe při znalosti kontextu, ale to mi přijde jako spíš z říše snů.

Samozřejmě mi na JS prototypování vadí, že funkce v objektech neví čí jsou. To pak vede k nutnosti neustále hlídat, zda sebou tahám kontext objektu (this) nebo ne. Funkce v closure naopak vědí čí jsou, je to víc OOP.

Ondřej Žára

Je docela jedno, jestli tvůj pohled považuje f1.m a f2.m za stejné funkce či nikoliv. Podstatné je, že pro interpret JS jsou zcela rozdílné, například i proto, že můžu napsat:

f1.m.neco = "ahoj";
f2.m.neco = 42;

A k žádné kolizi či přepsání nedojde. Jistě se tedy shodneme, že jsou identické sémanticky (dělají tu samou věc), ale tím to tak končí.

Jinak JIT je zkratka z „Just In Time“, což přesně vyvrací hypotézu, že by k překladu do strojového kódu došlo již během parsování. JIT stojí na tom, že se kompilje až „hot“ kód, tj. ten, který se ukázal jako často vykonávaný (a typově stabilní). Jen pro něj se pak za běhu – „just in time“ pustí ta (složitá, drahá) kompilace.

U parseru se ovšem může objevit AOT, „Ahead Of Time“ kompilace. Ta provádí překlad už při parsování, bez vykonání kódu. Jestli se něco takového děje u funkcí definovaných v konstruktoru, to upřímně nevím.

Ondřej Novák

pohybujes se porad v js dogmatu. Funkce je porad jeste js objekt, ktery ale interne bude mit skrytou konstantu a to vlastni kod, ktery ty tam nevidis. f1.m.neco tedy nic nedokazuje, neco je promenna, ktera nijak nemeni kod vlastni funkce, ani se nijak nepromitne do kontextu, dokud na nej pres this neukazes. O te skryte kostante celou dobu mluvim a to ze se nejakym zpusobem vytvori jen jednou pri parsovani kodu. Hloupa js implementace by tam mela cisty text tela te funkce, ktery by pri kazdem volani znova parsovala. Tohle snad delaji jen jednoduche skriptovace, u js bych to cekal chytrejsi… BTW v8 kompiluje pri parsovani, alespon to co jsem videl ve webkitu, nestalo se mi, ze bych mohl interpretaci js prochazet nativne pres engine, vzdy to byl strojak.

Ondřej Žára

O problematice JIT a AOT ve V8 dobře píšou na http://programmers.stackexchange.com/questions/246094/understanding-the-differences-traditional-interpreter-jit-compiler-jit-interp (poslední odstavec odpovědi). Je otázka, jestli lze považovat nějaký naivně hloupý a pomalý nativní kód za srovnatelný s tím, co následně — právě po zajištění typové stability a dalších podmínkách — vygeneruje JIT.

Ohledně sdílení kódu funkcí definovaných v konstruktoru — asi nebude obtížné pro tuto věc vyrobit malý benchmark, který vyrobí milion instancí jedním či druhým způsobem a profiler prozradí něco o zabrané paměti, což?

Ondřej Novák

jsem ochotnej pripustit ze casove naklady na vytvoreni jednoduchych malych objektu funkci neco skutecne casoove stoji a ze pri prototypovani tento cas usetrim. Otazkou je, zda to ma tu cenu a nevytvori to problemy jinde, treba v tom, ze se musim rozloucit se zapozdrenim (mohu to obchazet podtriztky coz neresi problem kdy potomek pouzije privatni promenne se stejnym jmenem jako predek)

Ondřej Žára

Co se rozstříštěnosti týče, ES6/ES2015 „class“ to řeší. A mimochodem, je to jen syntax sugar nad prototypy, takže i standardizační těleso preferuje sdílené metody před definicí v konstruktoru. Více například http://www.2ality.com/2015/02/es6-classes-final.html.

A použít to můžeš už dnes (babel, Traceur)…

caracho

Koncept if/esle s defaultni vychozi hodnotou, je narozdil od doporucovaneho z hlediska defenzivnoho programovani ten spravny. Kod to zjednodusuje, takze poznamaka o cistlenosti je dost sporna ale hlavne vam pak nehrozi ze pustite dale do vypoctu neinicializovanou promnenou s nepredvidatelnymi nasledky (na coz clanek naopak spravne poukazuje o par odstavcu jinde v pripade retezeni retezcu).

Honza

Defenzivní programování je pro roboty, ale ne pro lidi. Programování je obvykle týmová práce. Pokud ale chybí vzájemná důvěra mezi členy týmu, a je potřeba defenzivní přístup, mají lidé a firma velký problém.

NoxArt

To je stejná logika jako že unit testy jsou pouze pro vývojáře, co nepíší dokonalý a zcela bezchybný kód. Firma co testuje má velký problém, deklaruje tím, že vývojáři jsou neschopní…

Honza

Žádný vývojář nepíše dokonalý a zcela bezchybný kód. Unit testy jsou pro všechny vývojáře. Firma, kde vývojáři píší unit testy má mnohem méně problémů.

Nicméně pokřivená firemní kultura vývoje, kde se netestuje (případně testy nepíší přímo vývojáři, ale testeři), jde obvykle ruku v ruce s defenzivním programováním.

NoxArt

„Žádný vývojář nepíše dokonalý a zcela bezchybný kód“ – přesně tak, děkuji :)
Důvěra je naprosto nesmyslná metrika pro uvažování jestli programovat defensivně nebo ne. Lidé jsou omylní, i ten člověk (ať už je to kdokoli) co napíše test je omylný, takže není důvod zavrhovat přístup který se snaží chyby ještě více minimalizovat.

Vojtěch Mikšů

Místo JSLint bych doporučil ESLint (http://eslint.org), který se poučil s JSLintu, nabízí pokročilejší analýzu a hlavně skvělou rozšiřitelnost (podpora Babelu, JSX atp). Samozřejmě také nabízí integrace do všech běžných editorů. Jinak neexistuje rozumný důvod, proč ES6 nepoužívat s Babelem už dnes.

Aleš Hájek

použijte defenzivně Dart :-)))

Aleš Hájek

tiskni(text, true); místo tiskni(text, zarovnat); když změníte logiku funkce, a chyba v ní bude v reakci na druhý parametr, tak pokud to bude konstanta, máte ztíženou šanci ji najít. Pokud je to proměnná, která v názvu nese význam, je nalezení chyby a výskytu volání funkce mnohem jednodušší.

Tomáš Weiss

Co takhle použít Flow?

Ondřej Žára

Sice pozde, ale zrovna se tu objevilo vyjadreni od nejpovolanejsiho :-) https://twitter.com/BrendanEich/status/636689071465598976

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.