Prasit, či neprasit?

…aneb Lightbox implementovaný na dva způsoby. Ukážu vám dvě implementace notoricky známého widgetu Lightbox. Ta první je maximálně naivní, ta druhá sofistikovaná. Obě jsou postavené nad Google Closure Library, což ale není to podstatné. Podstatné je, jak jsou napsané.

Na Githubu naleznete zdrojové kódy k článku.

Ten první způsob lze nazvat “Co na mysli, to na klávesnici.” Žádné velké přemýšlení, rovnou píšeme kód. Druhý způsob je jiný, vyžaduje přemýšlení dřív, než napíšeme jediný řádek.

Prvním stylem je napsaná většina jQuery pluginů. Ten druhý využívá třídy, dependency injection, dokumentační komentáře, unit testy, a je implementovaný TDD stylem vývoje. Smyslem článku není obhajovat jeden styl vývoje na úkor druhého, naopak! Oba příklady ilustrují oprávněné způsoby vývoje software, a my si ukážeme některé techniky, které je odlišují. Na závěr se zamyslíme, kdy kterému dát přednost.

Co je špatný kód?

Snad každý instinktivně cítí, co je špatný kód. Je to impress.js? Z jednoho úhlu pohledu opravdu ne. Je to perfektní, minimalistická, vylepšená reimplementace core feature prezi.com, která navíc nepotřebuje Flash. Neobsahuje ani žádné významné code smells.

Z jiného úhlu pohledu se ale jedná o tragicky špatný kód. Dovedete si představit, že byste jej použili ve svojí aplikaci? Bez copy&paste a kompletní reimplementace? Stěží. Impress.js je technologické demo, prototyp. Netestovatelné, znovu nepoužitelné, a nejen to.

Ale dost teorie, sem s tím ukázkovým kódem.

Naivní ligthbox

(Ukázkový skript)

To, co vidíte, je základní princip lightboxu.

Click na odkaz s lightbox rel attributem zobrazí view s poloprůhledným pozadím, a přes něj obrázek s ovládacími prvky next, previous, close a title odkazu.

Podívejte se na kód, myslím, že je velmi srozumitelný. Dá se číst odshora dolů jako povídka. Je tedy takový kód v pořádku?

Zkuste se zamyslet, jak by kód vypadal, kdyby se dále rozvíjel ve stejném duchu. Chceme přidat animace. A list malých náhledů všech obrázků ve spodním pruhu. A možná nějaké další informace týkající se obrázku aktuálně vybraného. A možnost obrázek označit. A otočit. A tagnout v něm někoho.

A teď si představme, že chceme mít více verzí samotného lightboxu, jednu pro přihlášeného uživatele, druhou pro nepřihlášeného uživatele. Kolegu ve firmě náš výtvor zaujal, protože korektně zobrazuje fotky i na malých zařízeních (což 95 % lightboxů nezvládá), a chtěl by lehce modifikovaný lightbox pro svůj e-shop. Možností je nespočet, a pokud budeme lightbox rozšiřovat prostým přidáváním metod, záhy se náš rozkošný malý scriptík změní ve velikou kouli bláta, ulpívající na programátorově noze. Co s tím? Na scénu přichází sofistikovaný lightbox.

Sofistikovaný ligthbox

(a opět odkaz na ukázkový kód)

Sofistikovaný lightbox se snaží už od začátku i do tak malého kódu, jakým naivní lightbox je, vnést trochu řádu. Programování není o ničem jiném. Princip „jak psát správně velké aplikace“ je jednoduchý: nepsat velké aplikace. Rozbít velkou třídu do menších, co spolu komunikují. Prostě mít solidní objektový návrh.

Takže znova, a tentokrát začneme testy.

TDD a unit testy

Test driven development. Svatý grál vývoje, vychvalovaný až do nebe, každý ho používá.

Však to znáte: v hlavě si představíte, co by kód měl dělat (tady bude takový a takový formulář), napíšete kód, přepnete do prohlížeče (bavíme se o webu), dáte F5 a hned vidíte výsledek. S uspokojením dáme ALT-TAB, v editoru napíšeme další řádky kódu. A zase ALT-TAB, F5, ALT-TAB, F5… S tím jak aplikace kyne, množství ručních na-oko-testů roste.

Jak je zřejmé, každý testuje. Nedovedu si představit, že by někdo nasadil kód, který by alespoň jednou ručně nevyzkoušel. Problém je v tom “v hlavě” a “ručně”. Nebylo by lepší mentální obrazy raději zatavit do kódu, a místo ručně testovat automaticky? Ano. Ale tohle dělá skutečně málokdo.

Proč se tak málo píšou testy? Důvody jsou dva.

  1. Lidé neumí psát kód tak, aby se dal dobře a rychle testovat.
  2. Lidé si neumí nastavit vývojové prostředí tak, aby je spouštění testů neobtěžovalo.

V textu se soustředím na bod první.

@stevenmak: QOTD: „If you think test-first is expensive, try debug-later“” Even worse, try ‚debug after customer defect report‘ – https://twitter­.com/#!/dmatej/sta­tus/139994293082398721

Jak psát testovatelný kód

Začneme tím, že si představíme z jakých dílků by se lightbox měl skládat, a napíšeme na to test. Pokud se podíváme na naivní implementaci, je zřejmé, že lightbox se skládá z něčeho, co poslouchá bublající click eventy, a na ten správný zobrazí view. Lightbox agreguje anchorClickHandler a faktory metodu, která nám na požádání vytvoří view. Test může vypadat třeba takto:

suite 'este.ui.OOPLightbox', ->

    suite 'OOPLightbox.create', ->
        test 'should create lightbox instance with object graph', ->
            lightbox = OOPLightbox.create()

            lightbox.should.be.instanceof OOPLightbox
            lightbox.anchorClickHandler.should.be.instanceof ooplightbox.AnchorClickHandler
            lightbox.viewFactory.should.be.instanceof Function

https://gist.git­hub.com/1578979

Spustíme test (v některém dalším článku vám ukážu, jak mít testy, co se spouštějí samy). Test neprojde, protože použité třídy a metoda create ještě neexistují. Tak je to správně, nikdy nevěřte testu, který neselhal! Za pozornost stojí, že jsme začali s třídami, a ne vlastní logikou lightboxu. To je také správně, protože nemůžeme přeci začít psát metody, když nemáme třídy, do kterých bychom je psali. To, že jsme začali factory funkcí, ilustruje jeden důležitý aspekt vývoje OOP aplikací, který bývá často nepovšimnut, především u zbastlených aplikací. Míchání logiky stavby aplikace s její business logikou. Lightbox je také aplikace, i když maličká. Nějakým způsobem pospojovat dílky, to je úkol pro factory metodu create. Chceme trochu jiný lightbox? Třeba s view pro registrovaného uživatele? Použijeme jinou factory metodu.

Tohle mimo jiné znamená, že se ve vašem kódu, implementujícím business logiku, nikdy nesmí objevit operátor new.

Řekl jsem nikdy? Ok, lhal jsem. Pokud operátor new instancuje value object, případně jiný leaf object grafu, zkrátka a polopatě řečeno, pokud instancujeme něco, co už nic jiného dalšího neinstancuje, pak je operátor new v pořádku. Nebude nám to komplikovat testování v izolaci, a docela jistě nepotřebujeme ani vlastní factory.

Testování v izolaci a dependency injection

Zmínil jsem testování v izolaci, a.k.a. unit testy. Díky testování v izolaci není nutné vizuálně ověřovat, zdali napsaný kód dělá, co má. Pak se taky nesmírně zrychluje provedení všech testů. To, co testy zdržuje, zpravidla není to, co nás v testech zajímá. To, co testy zdržuje, je práce se skutečnými soubory, skutečnou sítí, skutečným prohlížečem. V unit testech nás ale zajímá jen náš vlastní kód, ne to, jestli prohlížeč XY správně implementuje attachEvent. Podívejte se na anchorclickhan­dler_test.cof­fee (link).

AnchorClickHandler je komponenta, která zapouzdřuje funkcionalitu lightboxu. Sleduje bublající click event, a pokud se kliklo na ten správný odkaz, sama odpálí vlastní click event, v jehož vlastnosti anchors jsou všechny odkazy lightboxu. Klasická event delegation technika, díky které není nutné registrovat listenery na všechny odkazy ve stránce, lightboxy tak mohou být klidně generované ajaxem.

Kdybychom v testu pracovali se skutečným DOM modelem, a ne jeho mockem, nejenže bychom zaváděli do testu externí závislosti, tudíž by se jednalo o testy integrační, ale testy by se nám i zpomalily. Aby bylo jasno, pokud provedení všech testů bude trvat déle než pár sekund, bude nás čekání na jejich výsledek rozptylovat (co je asi nového na Twitteru?).

Aby bylo možné třídu od externích závislostí oprostit, je nutné jí všechny závislosti předat explicitně, typicky přes konstruktor. Třídy se musí na své závislosti aktivně ptát. Nenechte je jejich závislosti pasivně vyhledávat. V tom nám pomůže technika dependency injection, hezky stručně shrnutá zde. A zde je příklad v Coffeescriptu:

# Dependency injetion example.
class House
    # We are asking for things in contructor.
    constructor: (kitchen) ->
        @kitchen = kitchen

# Service locator antipattern example.
class House
    constructor: ->
        # our class is looking for kitchen
        @kitchen = new Kitchen

# another point of view

# house is frozen
class House
    constructor: ->
        @kitchen = new Kitchen

# now house can have any kitchen
class House
    constructor: (kitchen) ->
        @kitchen = kitchen

https://gist.git­hub.com/1584972

It´s always important to emphasize that dependency injection is just plain-old good OO design – Markos Charatzas

Psaní testů před samotným kódem a důsledné vyvarování se service locatorů znamená, že budeme schopni instancovat každou část aplikace v izolaci. Nejen my, ale i ostatní. V testech, i kdekoliv jinde. To je klíč ke skutečné znovupoužitel­nosti kódu.

Psaní testů na už existující kód je ošidné. Můžeme napsat nefunkční test, a ani se to nedovíme, protože neuvidíme test spadnout. Můžeme dokonce zjistit, že napsat test není vůbec možné, nebo je to obtížné. Pokud o takový netestovatelný kód stojíte, zde najdete pár tipů, jak na to.

A ještě jedna poznámka: Podívejte se na mock DOM elementu (viz zde). Jak víme, které metody má mock mít? Nevíme. Nepotřebujeme to vědět. Spustíme test, a chybové hlášky nám to samy odhalí.

TypeError: Object #<Object> has no method ‚attachEvent‘

Provázání tříd

V našem sofistikovaném lightboxu máme tři třídy: Ligthbox, AnchorClickHandler a View. Jak spolu třídy komunikují? V zásadě existují dvě možnosti – voláním metod nebo reakcí na události. Náš AnchorClickHandler vyhazuje událost click. Klidně by ale mohl volat metodu show na injectované instanci View (nebo čehokoliv jiného). Kdy zvolit který způsob? Zvolte způsob vám v danou situaci pohodlnější, sofistikovný lightbox demonstruje oba. Vodítkem vám může být třeba when to use events.

Poznámky k implementaci

Oba příklady jsou napsány nad Google Closure Library. K testování byl užit testovací framework Mocha, s assert frameworkem should.js. V repository najdete zdrojový kód jak v Javascriptu, tak v Coffeescriptu. Ve svém dev prostředí mám všechny testy spouštěné automaticky při uložení souboru. Jen tak docílíte toho, že vás test driven development nebude vytrhávat z flow a vy se budete moci plně soustředit na svou práci zábavu, aniž byste opustili editor.

Google Closure Library

Oba příklady také demonstrují, že rozsáhlá Google Closure Library, knihovna na které jsou postaveny nejsložitější ajaxové aplikace, lze použít i na maličké projekty. Však každý velký strom vyrostl ze semínka. Pro mne je Google Closure Library stejně praktická a příruční záležitost jako Nette pro Davida Grudla http://phpfas­hion.com/tohle-kurva-ani-omylem.

Pokud se chcete o knihovně Google Closure Library dozvědět více, a pokud chcete vědět, jak si správně nastavit testovací prostředí, navštivte nový kurz Daniela Steigerwalda „Google Closure a test driven development“.

Hypované mikroframeworky, stejně jako tweet sized snippety, jsou – až na naprosté výjimky – roztomilými hříčkami, pro vývoj aplikací nevhodné. Jen pro zajímavost, zkompilovaný sofistikovaný lightbox má něco kolem 6kb.

Závěr

V zájmu udržení rozsahu článku jsem zmínil jen to, co je podstatné k pochopení přiložených příkladů. Záměrně jsem opomenul složitější témata jako IOC, MVC, BDD a další, což si nechám pro články budoucí. Nejbližší budoucí článek se bude věnovat popisu a srovnání některých javascriptových MVC frameworků.

Závěrečná otázka: prasit, nebo ne?

Tento článek vznikl stejným způsobem jako doprovodný kód. Nejprve jsem „naivně“ naprasil hrubou strukturu, až pak jsem jej vypiloval k přečtení, zasadil věci do kontextu, doplnil odkazy, komentáře. Určitě je tak pro vás mnohem přínosnější. Jen mi to zabralo více času…

Neexistuje jediná správná odpověď. Všechno je tradeoff, kvalita kódu je jen jedna z mnoha proměnných, a vy sami musíte určit, jakou zrovna tato má váhu. Moc pěkně to shrnul Uncle Bob Martin.

“When designs are cheaper than builds, e.g. a skyscraper or a bridge, big up front design makes good sense.” https://twitter­.com/#!/uncle­bobmartin/sta­tus/156020534382047232

“But when builds are cheaper than designs, e.g. software or oil painting, short and iterative design and build steps are best.” https://twitter­.com/#!/uncle­bobmartin/sta­tus/156021350492934144

Do češtiny převedeno: Dvakrát měř, jednou řež. Ale jenom pokud je řezání alespoň dvakrát těžší než měření.

Independent software gardener, libertarian, web applications consultant and trainer. Google Developer Expert since 2012.

Komentáře: 119

Přehled komentářů

Radek Miček Klíč ke znovupoužitelnosti a testovatelnosti
Nox Re: Klíč ke znovupoužitelnosti a testovatelnosti
Radek Miček Re: Klíč ke znovupoužitelnosti a testovatelnosti
Jirka Chmiel Re: Klíč ke znovupoužitelnosti a testovatelnosti
JS Re: Klíč ke znovupoužitelnosti a testovatelnosti
ggael Re: Klíč ke znovupoužitelnosti a testovatelnosti
Radek Miček Re: Klíč ke znovupoužitelnosti a testovatelnosti
JS Re: Klíč ke znovupoužitelnosti a testovatelnosti
blizz Re: Klíč ke znovupoužitelnosti a testovatelnosti
Daniel Steigerwald Re: Klíč ke znovupoužitelnosti a testovatelnosti
Pavel Šimerda Re: Klíč ke znovupoužitelnosti a testovatelnosti
Radek Miček Re: Klíč ke znovupoužitelnosti a testovatelnosti
blizz Re: Prasit, či neprasit?
Lol Phirae Re: Prasit, či neprasit?
Palo Re: Prasit, či neprasit?
bakerman Re: Prasit, či neprasit?
Riff Re: Prasit, či neprasit?
Vrtak-CZ should.js
Jiří Knesl Re: should.js
Miloslav Ponkrác Re: should.js
Blb Re: should.js
alancox Re: should.js
Daniel Steigerwald Re: should.js
alancox Re: should.js
Daniel Steigerwald Re: should.js
alancox Re: should.js
Jiří Knesl Re: should.js
Oldisy3 Re: should.js
gilhad Re: should.js
brablc Re: should.js
alancox Re: should.js
Daniel Steigerwald Re: should.js
David Grudl Re: should.js
František Kučera Re: should.js
wtf_prezdivka? Re: should.js
David Grudl Re: should.js
Martin Malý Re: should.js
JS Re: should.js
David Grudl Re: should.js
JS Re: should.js
František Kučera SQL!
David Grudl Re: SQL!
kvr Re: SQL!
brablc Re: should.js
Oldisy3 Re: should.js
Jiří Knesl Re: should.js
Radek Miček Re: should.js
Jiří Knesl Re: should.js
Radek Miček Re: should.js
Ladislav Thon Re: should.js
Daniel Steigerwald Re: should.js
Michal Augustýn Re: Prasit, či neprasit?
Aleš Roubíček Závěr
Neznamy Hrdina Re: Závěr
Petr Janda (twitter.com/petrjanda) Dependency injection v JavaScriptu?
Aleš Roubíček Re: Dependency injection v JavaScriptu?
Petr Janda (twitter.com/petrjanda) Re: Dependency injection v JavaScriptu?
Aleš Roubíček Re: Dependency injection v JavaScriptu?
Petr Janda (twitter.com/petrjanda) Re: Dependency injection v JavaScriptu?
Aleš Roubíček Re: Dependency injection v JavaScriptu?
Petr Janda (twitter.com/petrjanda) Re: Dependency injection v JavaScriptu?
Aleš Roubíček Re: Dependency injection v JavaScriptu?
Petr Janda (twitter.com/petrjanda) Re: Dependency injection v JavaScriptu?
Aleš Roubíček Re: Dependency injection v JavaScriptu?
Petr Janda (twitter.com/petrjanda) Re: Dependency injection v JavaScriptu?
Nox TDD, BDD
Ladislav Thon Re: TDD, BDD
Aleš Roubíček Re: TDD, BDD
Nox Re: TDD, BDD
Aleš Roubíček Re: TDD, BDD
Nox Re: TDD, BDD
Palo Nezprasit?
tdvorak Re: Nezprasit?
Palo Re: Nezprasit?
Nox Re: Nezprasit?
Palo Re: Nezprasit?
blizz Re: Nezprasit?
alancox Re: Nezprasit?
David Grudl Re: Nezprasit?
Oldisy3 Re: Nezprasit?
skrat Test first non sens
skrat Re: Test first non sens
Daniel Steigerwald Re: Test first non sens
skrat Re: Test first non sens
ijacek Re: Test first non sens
skrat Re: Test first non sens
Daniel Steigerwald Re: Test first non sens
Satai Re: Test first non sens
Daniel Steigerwald Re: Test first non sens
Satai Re: Test first non sens
Daniel Steigerwald Re: Test first non sens
skrat Re: Test first non sens
alancox Re: Test first non sens
Aleš Roubíček Re: Test first non sens
František Kučera Re: Test first non sens
Satai Re: Test first non sens
František Kučera 100% bezchybnost
Satai Re: 100% bezchybnost
František Kučera 100% bezchybnost je neprokazatelná
Satai Re: 100% bezchybnost je neprokazatelná
should.be.irrelevant() Re: 100% bezchybnost je neprokazatelná
Aleš Roubíček Re: 100% bezchybnost
František Kučera Re: 100% bezchybnost
Aleš Roubíček Re: 100% bezchybnost
František Kučera Re: 100% bezchybnost
David Grudl Re: 100% bezchybnost
Neznamy Hrdina Re: 100% bezchybnost
rony testovanie
David Grudl Re: testovanie
František Kučera jednotkové vs. testování
Michal Till Re: jednotkové vs. testování
František Kučera Re: jednotkové vs. testování
Pepa nééé
František Kučera Re: nééé
Pepa Re: nééé
František Kučera Re: nééé
Pepa Re: nééé
Oldisy3 Re: nééé
5o OOP, TDD, CI
Zdroj: https://www.zdrojak.cz/?p=3594