pondělí 23. září 2013

Snižování rizikovosti softwarových projektů

… aneb díky zpětné vazbě rychleji kupředu

Děláme to dobře? Bude se zákazníkovi a uživatelům výsledek líbit? Máme hodně nebo málo bugů? Stíháme harmonogram projektu? Jaká jsou v projektu největší rizika? Ustojí architektura systému reálnou zátěž? ...

Množství neúspěšných softwarových projektů je pro nás varováním, abychom byli obezřetní a neustále hledali způsoby, jak rizikovost vývoje software snižovat. Důsledkem neúspěšného zvládání rizik mohou být obrovské finanční ztráty při produkování nestabilního software. Za dobrý výsledek se nedá považovat ani software rigidně splňující původní požadavky, avšak uživatelsky nepřátelským způsobem. A v mnoha případech dokonce projekty umírají ještě dříve, než se dostane prvotní verze mezi reálné uživatele.

Jsem přesvědčen, že kouzelnou instancí úspěšných projektů je mimo jiné i kvalitní zpětná vazba. Je nutné, abychom byli schopni zpětnou vazbu získávat v co nejkratší době a nezkreslenou a to navíc po celou dobu trvání projektu. Díky zpětné vazbě lépe držíme správný směr a rychleji řídíme projekt směrem k úspěšnému výsledku. Dále popisuji vybrané techniky, které nám pomohou tuto drahocennou komoditu lépe získávat.

Jednotkové testy (Unit Tests)

Zdrojový kód je živý organismus, který prochází neustálým vývojem a změnami. Má-li být projekt úspěšný, je kvalitně navržený zdrojový kód s minimální chybovostí naprosto esenciálním požadavkem. Psaní jednotkových testů je technikou, která nám dává do rukou prostředek pro získávání zpětné vazby o stavu kódu v řádech desítek sekund.

Zpětná vazba je dvojího typu. Dokážeme ověřit, zda-li kód opravdu dělá to, co od něj očekáváme - na co není napsaný test, to jako by v systému ani neexistovalo. Za druhé, dostáváme důležitou informaci o kvalitě návrhu tříd a potažmo celého systému. Na oba indikátory bychom měli být schopni rychle reagovat a problémy řešit.

Programování bez psaní jednotkových testů je mizerná loterie, ve které se obvykle prohrává. Vede k objevování chyb složitějším způsobem a v pozdějších fázích vývoje. Vkrádá se nejistota a strach zasahovat do existujícího kódu, který není pokrytý testy. Téměř se nerefaktoruje, vznikají duplicity a logickým vyústěním je zvyšování nákladů na celý projekt. Bez testů není možné aplikovat některé z dále popsaných technik.

Podobně jako jednotkové testy, potřebuje náš projekt testy integrační. Jedná se o kategorii testů, které prověřují vzájemnou spolupráci více reálných tříd v požadovaných scénářích.

Průběžná integrace (Continuous Integration)

Podívejme se na práci programátora z perspektivy celého vývojového týmu. Jeho chyby totiž mohou nepříznivě ovlivnit zbytek týmu. Nesoustředěný, spěchající nebo unavený kodér promítne změny na server a odchází domů (v horším případě vypíná telefon a odjíždí na třítýdenní zahraniční dovolenou). Nevšimnul si ale, že aplikace nejde zkompilovat nebo že rozbořil pár jednotkových testů. Jiný člen týmu si vezme změny ze serveru a v ten okamžik má problémy i on. Nezkompiluje, prohodí pár jadrných slov (které ve slovníku spisovné češtiny nenajdeme) a začne hledat pachatele. Nakonec se situace nějak vyřeší, ale zpomalení lidí a někdy celého týmu je vždy nepříjemné a drahé.

Řešením je zavedení techniky průběžné integrace. Jedná se o kontrolní mechanismus, který po každém promítnutí změn na server spustí posloupnost ověřovacích kroků. Běžně se provádí kompilace celého projektu, spouští se jednotkové testy, hlídají se pravidla kladená na formátování kódu, provádí se statická analýza kódu, apod. V řádu několika minut dostává programátor, který změny propagoval na server, zpětnou vazbu o tom, že jeho balík změn je nebo není problematický. V zájmu celého týmu by měl na případný problém okamžitě reagovat a uvést verzi na serveru do korektního stavu. Nebrzdit zbytek týmu musí být nejvyšší prioritou.

Týmy, které nemají rozjetý CI mechanismus, se často dostávají do záseků. Reakce na problémy se prodlužují. Dohledávání příčin je složité, protože není vždy snadné určit, který balík změn problém způsobil. Každá propagace změn na server je provázena lechtivým pocitem v podbřišku. Strach a nejistota dlouhodobě paralyzují práci týmu. Není také zřejmé, zda-li je aktuální serverová verze v dobrém stavu a tedy nasaditelná např. do testovacího prostředí.

Revize (Reviews)

Často nedoceňovanou technikou je vzájemné revidování práce. Revidovat můžeme specifikaci požadavků, návrh řešení, zdrojový kód nebo jiné výstupy vaší práce. Odpůrci argumentují vysokou časovou náročností a tedy malou ROI. Pokud to však myslíme s kvalitou aplikace vážně, je tato technika nepostradatelná. Téměř vždy je pohled na řešený problém očima někoho jiného užitečný. Např. při revizi kódu můžeme připomínkovat použitý algoritmus, řešit čitelnost kódu, objevit chyby nebo jiná potenciálně problematická místa. Revizemi si členové týmu vzájemně předávají zkušenosti, učí se a zvyšují bus-factor (zastupitelnost) vývoje částí aplikace.

Kdy a v jakém rozsahu je vhodné provádět revize? Vždy, když je dokončen úkol menšího rozsahu. Ideálně práce, která se vejde do jednoho dne. V méně zkušených týmech mohou být revize delší - objeví se více připomínek. Např. v našem týmu je poměr času strávený prací a její revizí asi 10 : 1. Poměr kolísá v závislosti na složitosti úkolu a aktuální míře “hnidopišství” revizora. Preferujeme spíše méně formální revize, buď za účasti programátora (při složitějších věcech, kde je vhodný vysvětlující komentář) nebo bez něj.

Párové programování (Pair Programming)

Některé věci se lépe dělají ve dvou - třeba programování. Práce v páru je kontinuální diskuze nad problémem a zvoleným řešením. Zpětná vazba je okamžitá. Dochází k vzájemnému revidování a téměř s určitostí bude výsledek společné práce vždy kvalitnější než při individuálním přístupu. Revidování je elementární činností této techniky a platí pro něj všechna pozitiva popsaná výše.

Ne všechny týmy mají vhodné prostředí pro aplikování této techniky. Efektivita je přímo úměrná “sociální kompatibilitě” členů týmu. Někdo je natolik introverdní, že práce ve dvojici jej stresuje a nedokáže ze sebe dostat to nejlepší. Také ne každý úkol je vhodné dělat ve dvojici. Obecně se dá říci, že čím je úkol složitější, tím je párování užitečnější.

Zákazník na pracovišti (On Site Customer)

Zákazník přesně ví co chce, co nejpodrobněji nadefinuje požadavky na systém, udělá se analýza, pak estimace, podepíše se smlouva. Vývojový tým dostane zadání, několik měsíců na něm dělá bez asistence zákazníka a poté, jako mávnutím kouzelného proutku, představí zadavateli funkční a skvělé řešení. … Tak a teď tu o Červené karkulce.

Dobrá tedy, domluvíme raději pravidelné týdenní pracovní porady se zástupcem zadavatele. Lepší, ale stále to není ono. Co takhle mít schopného člověka od zákazníka k dispozici kdykoliv? Mít možnost mu zavolat nebo se s ním sejít ihned, jak si to situace vyžádá? Mnohem lepší. Nebo raději, kdyby fyzicky seděl s týmem v jedné místnosti? Ideální.

Zákazník nám platí a pouze na jeho hodnocení záleží, zda-li bude náš projekt úspěšný. Na začátku vývoje obvykle nemáme dostatečně jasnou představu o tom, jak by měl výsledek vypadat. A i když máme, tak se v průběhu určitě změní. To je zákon softwarového vývoje. Zafixovat požadavky na začátku a dogmaticky trvat na jejich dodržování je možná dobré s ohledem na dodržení podmínek smlouvy, ale málokdy pro výslednou kvalitu. Nebraňme se změnám zadání, buďme na ně připravení. Je naprosto legální, že požadavky se v čase vyvíjí podle toho, jak se tým i zadavatel učí rozumět problémové doméně. Zapojme zákazníka co nejvíce do vývojového procesu a budou z toho profitovat obě strany.

U některých typů projektů, kdy nemáme skutečného zákazníka, je také vhodné zadefinovat interního zákazníka, který bude vystupovat v podobné roli jako skutečný zákazník. Bude zastupovat uživatele vyvíjené aplikace a bude zodpovědný za akceptaci výsledku. Měl by nám být nápomocen při diskuzích a připomínkování a neměl by být z našeho vývojového týmu.

Akceptační testování

Akceptační testování je nezbytný proces pro ověření toho, že aplikace dostála závazkům vyplývajících z požadavků. Akceptační testy tvoří zároveň aktuální dokumentaci k systému. Ideální akceptační testy jsou automatizované, tedy kdykoliv snadno spustitelné. Pokud má aplikace GUI, pak zřejmě bude nutné akceptační testy provádět také ručně. Akceptační testy měl psát zástupce zákazníka ve spolupráci s členy QA týmu. Ne programátor.

Správně napsané akceptační testy dodají kvalitní zpětnou vazbu o použitelnosti a stabilitě aplikace. Procházení jednotlivých testovacích případů je také měřítkem celkového postupu projektu. Získáváme informaci o tom, kolik práce je hotovo a kolik ještě zbývá. Ale znáte to, Paretův princip náš optimismus ve finále stejně zchladí. :)

Malé verze (Small Releases)

Hodně projektů se prodražilo a mnohé úplně zkrachovaly, protože se dostaly do obrovských problémů s vydáním první funkční verze. Na první pohled vypadá snaha uvolnit “superverzi” aplikace jako dobrý nápad. Projektový tým chce vydat aplikaci až v okamžiku, kdy bude splňovat všechny must-have požadavky a ty budou navíc propracovány do nejmenších detailů. Ale my to takhle určitě nedělejme.

Vývoj software je jako většina profesí především o emocích, motivaci, komunikaci, důvěře a dalších netechnických záležitostech. Lidem se mnohem lépe plní rozsáhlejší úkoly, pokud jsou rozděleny na více menších. A navíc každý úkol by měl být zakončen dosažením cíle. Naším prvořadým cílem je vydat aplikaci a dostat ji k uživatelům. Teprve v tomto okamžiku má naše práce nějakou reálnou hodnotu. Do té doby nemáme nic.

Uživatelé ocení, pokud jim brzy dodáme verzi aplikace, která jim přináší nějaký užitek. Navíc rychle získáme zpětnou vazbu z reálného provozu, která je pro nás nejcennější. Vydávejme verze tak často, jak je potřeba. Funkcionalita nemusí být dotažena k dokonalosti, ale nesmíme vypouštět bugy. Pro časté automatizované uvolňování verzí je nutné mít vybudovánu kvalitní infrastrukturu pro Continuous Delivery. Jde o automatizaci vytváření verze, spouštění validačních mechanismů, nasazování do testovací prostředí a ve finále také do produkce. Obsahuje některé procesy známé z Continuous Integration.

Prototypování (Prototyping)

Softwarové projekty jsou plné rizikových míst. Rizikem se může stát například použití nové technologie, návrh architektury, problematický framework, komunikace s novým fyzickým zařízením, napojení na špatně zdokumentovanou službu, apod. Zkušenosti vývojářů se projeví právě ve schopnosti dopředu tato rizika vytipovat a prověřit. Nejrizikovější části projektu řešme co nejdříve. Jako vhodná metoda pro validaci rizik se obvykle jeví prototypování.

Prototyp je zjednodušená a tedy levná varianta aplikace nebo její části, která nám pomůže prověřit potenciální riziko. Při vytváření prototypu není nutné trvat na vysoké interní kvalitě. Důležitá je především rychlost vytvoření a získání zpětné vazby o problému. Po ověření problému život prototypu končí. Produkční kód se začne budovat znovu, tentokrát v produkční kvalitě.

Často se prototypuje uživatelské rozhraní. V takovém případě nejde ani tak o prověřování rizika, zda-li něco nepůjde, ale spíše o ujasnění představy o výsledném produktu. Levně můžeme připravit více variant řešení.

Minimální tržní produkt (Minimum Viable Product)

Jak už bylo popsáno výše, měli bychom se vyhnout snaze dodat veškerou zamýšlenou funkcionalitu najednou. Vývoj by byl zdlouhavý a zpětnou vazbu z reálného provozu bychom získali příliš pozdě. Technika Minimum Viable Product nabádá k tomu, abychom zadefinovali minimální rozsah funkcionality, který reprezentuje vizi vašeho produktu, tu rychle vyvinuli a nasadili do provozu. Získáme tak ohlasy od skutečných uživatelů z reálného provozu. Začneme se rychle učit, jak na produkt uživatelé reagují, jaké by se hodily další funkce a které funkce jsou nevyužívané a tedy nadbytečné. Ideální použití je v situaci, kdy je uživatelská spokojenost skutečně naším prvořadým cílem. To znamená vždycky. Státní IT projekty se nepočítají. ;)

Další členové rodiny technik pro získávání zpětné vazby od skutečných uživatelů jsou Win-loss Analytics, Beta Programs, Focus Groups nebo Market Interviews.

Iterace (Iterations)

Rozdělit řešení rozsáhlého projektu na menší části s jasně definovanými akceptačními kritérii je základem úspěchu softwarového projektu. Zapomeňte na smrtící vodopád a naučme se organizovat práci do iterací. Iterace dostanou náš tým do potřebného pracovního rytmu a získáme důležitou zpětnou vazbu o jeho rychlosti a možnostech. Každá iterace (doporučená délka 1-3 týdny) začíná plánováním úkolů včetně estimace, pokračuje realizací a končí předvedením výsledků a retrospektivou.

První iterace mohou být o sesbírání potřebných informací, vytváření vývojové infrastruktury, zavádění agilních technik, prototypování, vytvoření základních projektových artefaktů, apod. V dalších fázích už řešíme konkrétní požadavky a začínáme produkovat první validní výstupy. Jako první si zvolme nejprioritnější nebo nejrizikovější požadavky, které nás donutí implementovat vertikálně skrze všechny vrstvy aplikace. Učíme se tak co nejrychleji rozumět všem aspektům realizace naší aplikace.

Komunikace v týmu (Team Communication)

Jejda, ty děláš na stejném úkolu jako já!
Vy jste poslední dny dělali tenhle úkol?! Ten má ale aktuálně nízkou prioritu!
Na pár dnů jsem se zasekl s tímhle. -- Proč neřekneš, zrovna tohle jsem nedávno řešil!

Dochází k podobným zmatkům i u vás. Nevíte přesně, kdo na čem aktuálně dělá? Pak je nezbytné zlepšit komunikaci v týmu. Je na zodpovědnosti vedoucího týmu, aby zajistil optimální komunikační prostředí a vzájemnou informovanost všech členů.

Využívejme techniky společných plánovacích schůzek, díky kterým všichni vědí, co se bude dělat a co je prioritní. Zabudujme do svého pracovního dne krátké týmové stand-up rychloporady. Jedná se o efektivní způsob, jak se vzájemně informovat o aktuálně řešených úkolech a problémech. Nezapomínejme na společné retrospektivy, kde se dozvídáme o úspěšnosti plnění našich plánů. Na retrospektivách je také prostor pro diskuzi nad pracovními postupy, které používáme. Nebojme se zahodit postupy, které nefungují a pouze nám přinášejí zbytečnou zátěž.

Postupnými malými změnami v našich pracovních postupech můžeme ve výsledku dosáhnout velkého zlepšení. A o to nám přeci jde. Vím to, protože jste tento článek dočetli až sem. ;)

sobota 14. září 2013

Život s debuggerem

… aneb debugování jako indikátor problémových postupů

Byl pátek třináctého, navíc 256. den v roce - svátek programátorů a já jej oslavil téměř 10-tihodinovou programovací šichtou. Po celý den jsem se pokoušel striktně dodržovat TDD přístup. Padající test, nejmenší nutná implementace, zelenáč. A takhle pořád dokola. Jako když rytmicky dýcháte - nádech a výdech. Práce šla od ruky a unit testy rodily produkční kód pěkně podle plánu. Za celý den jsem použil debugger snad jen dvakrát. V kontextu událostí jsem si utvítnul trochu provokativní tvít:

Akce rodí reakci a ta také přišla. Twitter je plný šikovných lidí se spoustou zkušeností z praxe a já jsem za jejich reakce moc rád. Zároveň je Twitter omezený na 140 znaků a to se pak těžko argumentuje. Dejte mi šanci utvítnutou myšlenku trochu vysvětlit formou tohoto příspěvku.

Dopředu je potřeba přiznat, že bych se bez možnosti debuggování aplikace určitě neobešel. Na druhou stranu pozoruju, že s postupným profesním vývojem se u mě nutnost používání tohoto nástroje postupně snižuje. Snažím se hledat postupy, které vnášejí do vývoje aplikace větší jistotu a menší chybovost.

Unit testy a TDD

K jedné z největších revolucí v mých pracovních návycích došlo v okamžiku, kdy jsem začal psát unit testy. Potenciál unit testů využijeme naplno až v kombinaci s TDD nebo nějakou jinou test-first technikou. Ta nás nutí přemýšlet dopředu o struktuře a funkcionalitě kódu. Rozkládat komplexnost řešení na malé samostatné scénáře použití. Jedním z klíčových aspektů je postupná, iterativně přidávaná funkcionalita. I při budování rozsáhlé aplikace metodou malých přírůstků je pravděpodobné, že budete mít stav věcí pod neustálou kontrolou. Určitě mnohem více, než když nakódujete desítky řádek kódu a pak se je snažíte najednou otestovat a integrovat.

Krůčky jsou tak elementární, že téměř ani není nutné spouštěný test debugovat. Třídy jsou testovány v izolaci a pokud navíc dodržujeme např. SOLID, rozpadá se nám funkcionalita do malých tříd s jasně definovanými zodpovědnostmi. Takové třídy se pak dobře testují. Práce bez debuggeru je mnohem rychlejší.

Integrační testování

Jistota z unit testů se přenáší do integračních testů, které jsou cílené na ověření spolupráce mezi více reálnými třídami. V případě dobře navržených rozhraní a podpory IoC kontejneru je celý proces integrace, opět po postupných krůčcích, transparentní a méně náchylný k chybám. Máme odzkoušené jednotlivé třídy? Pak zřejmě bude fungovat i jejich kompozice.

Při integraci se do akce dostávají také frameworky třetích stran. Míra nutnosti použití debuggeru pak bude zřejmě odvislá od naší znalosti jejich API a také od toho, jak dobře použitelné tyto frameworky jsou. Nerozumím používanému frameworku? Asi si hodně užiju debugování.

Čistý, přehledný a jednoduchý kód

Programátor dospívá v okamžiku, kdy se mu přestává líbit složitý kód a začíná hledat prostředky zajišťující jednoduchost. Skrze kód komunikujeme s počítačem a s ostatními členy týmu. A je to podobné jako jiné formy komunikace. V případě, že něčemu skutečně rozumíme, umíme to vyjádřit jednoduše a jasně. Dobré komunikační dovednosti posouvají programátora na vyšší úroveň a zvyšují jeho hodnotu pro týmovou spolupráci.

Pokud nezměníme firmu nebo náš projekt nekrachne, pak nás vlastní kód bude pronásledovat ještě dlouhé roky. Určitě znáte ten pocit, kdy se po delší době vrátíte ke staršímu kódu a nerozumíte mu. Dokonce se k němu někdy nechceme ani znát. To jsem opravdu napsal já? No nic, nezbývá než refaktorovat.

Že se nám podařilo napsat nesrozumitelný kód poznáme podle toho, že jsme nuceni často spouštět debugger a krokovat, abychom pochopili sémantiku kódu.

Rychlost nebo kvalita

Vždy preferujme dobrý návrh a udržovatelnost kódu nad rychlostí jeho vytváření. U dlouhodobých projektů se nám to vrátí mnohonásobně.

Někde na fóru jsem četl myšlenku:

Jednou z nejdůležitějších dovedností programátora je schopnost psát jednoduché kousky kódu správně napoprvé, bez nutnosti používat debugger.

Tato myšlenka nám dává návod, jak být současně rychlí. Naučit se malé elementární programátorské problémy řešit napoprvé správně a kompozicí z nich budovat složitější. Získat jistotu v základech naší práce - v algoritmizaci.

Diagnostika

Dobře zvolená diagnostika aplikace nám může ušetřit časově náročné ladění aplikace skrze debugger. Logování chování aplikace a především podrobný kontext chyby je neocenitelným pomocníkem pro rychlou navigaci k problémovému místu. Nespoléhejme se na důvěryhodnost hlášení od uživatelů, zajistěme si automatizovaně dostatek informací k problémům sami.

Techniky na detekci a předcházení chybám

Debugger je deratizérský nástroj určený pro odchyt otravných, obvykle v pátek odpoledne se objevujících, bugů. Ještě před použitím vlastního ladění nebo ihned poté co jsme chybu přes debugger detekovali, můžeme zkusit chybu izolovat. Na chybový scénář napíšeme jednotkový nebo integrační test. Fixnutí bugu tak odpovídá stavu, kdy nám začne procházet nově napsaný test.

Mnohem levnější, než nahánět bugy nahlášené od testerů a uživatelů, je bugům aktivně předcházet. Výše popsané techniky můžeme doplnit o revize kódu nebo přímo programovat v páru a tím code review dělat v reálném čase. Důležité je samozřejmě používat jednotné kódovací standardy a jmenné konvence, které urychlují orientaci a porozumění kódu všem členům týmu a snižují riziko špatného použití. Využívejme nástroje na statickou analýzu kódu, které umí najít spoustu potenciálně rizikových míst. Neignorujme kompilační warningy. Jsou to tiše tikající časované bomby, které se mohou změnit v nepříjemné chyby. Praktikujme společné vlastnictví kódu, které pomáhá šířit znalosti v týmu a kontinuálně vylepšuje code base. Využívejme průběžnou integraci na buildovacím serveru. Zlepšujme komunikaci na všech úrovních struktury projektového týmu.

Závěrem

Debugger jako nástroj má samozřejmě své opodstatnění. Ve vlastním zájmu bychom se ho měli naučit ovládat efektivně a znát všechny jeho možnosti. Zároveň je však třeba si uvědomit, že jeho časté nadužívání může indikovat zásadnější problémy v našich postupech a dovednostech. Vystupme občas z pracovního stereotypu a přemýšlejme nad tím, jak naši práci zefektivnit tak, abychom debugger - nástroj poslední záchrany - nemuseli používat příliš často.