Jste zde

Vysoce kvalitní kód

V tomto příspěvku se dozvíte, co to je vysoce kvalitní kód, proč je důležitý a na co si dát při jeho psaní pozor.

Na toto téma mě přivedla tato skvělá přednáška od STL zaměřená na generování náhodných čísel v C a C++11. Zmiňoval tam pojem absolutně korektní kód, což je kód, který za všech situací funguje korektně. To je jedna z vlastností vysoce kvalitního kódu.

Mimochodem, pokud si myslíte, že rand() je fajn funkce pro generování náhodných čísel, určitě se na tuto půlhodinovou přednášku mrkněte.

Co je to vysoce kvalitní kód?

Uvedu to na příkladu. Představte si, že píšete aplikaci a potřebujete využít cizí knihovnu, která poskytuje jistou funkcionalitu. Na Internetu objevíte dvě knihovny a rozhodnete se je vyzkoušet, abyste zjistili, kterou použít.

U první knihovny máte problém již při zapracování do projektu. Při překladu dostáváte podivné chyby, jejichž odladění a oprava si vyžádá jistý čas. Dokumentace je nevalné kvality a není vůbec nápomocná. Po pár zásazích do zdrojáků ale už vypadá, že to prošlo a můžete ji začít používat. Když však využijete některé její funkce, zjistíte, že při chybách vypisuje chybová hlášení přímo na výstup. Takže musíte přesměrovávat standardní výstup při práci s knihovnou, abyste tomuto zamezili. Někdy vám dokonce ukončí celou vaši aplikaci, protože při chybě zavolá exit(). To musíte taktéž ve svém kódu ošetřit, aby se knihovna u některých vstupů nevolala. Dále, u některých funkcí si vůbec nejste jistí, jak je použít. Aplikační rozhraní knihovny je jeden velký zmatek a nejste si jisti, v jakém pořadí máte dané funkce volat, abyste dosáhli kýženého efektu. Nikde nejsou žádné příklady. Při používání knihovny se musíte neustále dívat do dokumentace, protože je vše takové křečovité. Časem dojdete k tomu, že potřebujete knihovnu rozšířit o dodatečnou funkcionalitu. Podíváte se tedy na zdrojový kód knihovny. Ale ouha, cítíte se jako Alenka v říši divů. Kód je nepřehledný, nedokumentovaný a celkově je přidání nové vlastnosti noční můra. Knihovna dále nemá žádné testy, takže si nejste jisti, zda jste úpravami nezanesli do kódu chyby. Pořádně zpruzeni necháte první knihovnu na pokoji a mrknete na druhou knihovnu.

Druhá knihovna je něco úplně jiného. Je postavena tak, že její integrace do vašeho projektu je naprosto jednoduchá. Sama vás při sestavení informuje o chybějících závislostech a navrhuje vám řešení. V dokumentaci je krok po kroku popsáno detailní řešení možných problémů. Když knihovnu používáte, tak se cítíte jako doma. Jakoby vám tvůrce knihovny četl myšlenky. Vše funguje, jak očekáváte. V dokumentaci je detailně popsána každá funkce, co má za parametry a co od nich očekává, co vrací, jaké možné výjimky může vyhodit a obsahuje příklady použití. Nikdy není potřeba, abyste sami volali určité funkce v určitém pořadí; knihovna se o to sama interně postará. Při rozšiřování knihovny narazíte na detailní dokumentaci pro programátory, jak knihovnu rozšiřovat. Kód je přehledný, perfektně dokumentovaný a nemáte problém s jeho porozuměním. Kompletně celý kód knihovny je pokryt jednotkovými testy a tak se snadno můžete přesvědčit, že jste do kódu nezanesli chyby.

Kterou knihovnu byste použili? Až odpovíte na tuto otázku, zeptejte se sami sebe: když programuji, píši kód, který bude vypadat jako kód první knihovny, nebo jako kód druhé knihovny? Jste na svůj kód pyšní a nestydíte se za něj, nebo jej raději schováváte, aby jej nikdo neviděl a při pomyšlení, že by se jím měl někdo hrabat dostáváte kopřivku?

Proč je důležitý?

Vysoce kvalitní kód je něco, co odlišuje amatéry od profesionálů. Je to čistý kód, který funguje. Nejen že vypadá, že funguje, ale opravdu funguje. Ve všech případech. Je to snaha dělat věci správně. A mnohem více.

Pokud používáte cizí kód, tak chcete, aby byl kvalitní. Abyste nemuseli strávit hodiny svého času hledáním informací v nepřehledné (či dokonce neexistující) dokumentaci. Abyste nemuseli řešit, že kód v některých případech nefunguje tak, jak má. Každý chce používat kvalitní kód. Ten ale musí někdo napsat. A kdo, když ne vy? Když napíšete nekvalitní kód, tak stavíte své uživatele či spolupracovníky do pozice uživatelů oné první knihovny.

Kdyby kód knihovny openssl nebyl prasečina na druhou a kdyby dotyčný programátor korektně validoval všechny vstupy, tak by se žádný Heartbleed bug nemusel konat.

Mým snem je, abych se naučil psát a psal vysoce kvalitní kód. Jakožto programátor to cítím jako své poslání. Stát se opravdovým profesionálem.

Na co si dát pozor při psaní vysoce kvalitního kódu?

Při psaní vysoce kvalitního kódu je potřeba brát do úvahy spoustu faktorů a řídit se velkým množstvím pravidel. Z nich uvedu několik, které považuji za velmi důležité.

  • Návrh rozhraní. Vaše navržené rozhraní by mělo být krystalově čisté. Pro uživatele by mělo být radostí jej používat.
  • Tvorba dokumentace. Ke kódu je potřeba mít dokumentaci, která bude popisovat instalaci, konfiguraci, použití, příklady, popis rozhraní, tipy atd. Pokud bude kód někdo rozšiřovat či udržovat, tak je potřeba udělat i speciální dokumentaci týkající se implementace. U tříd a funkcí byste měli popisovat, co dělají, jaké mají parametry, co vrací, jaké jsou na parametry požadavky, co by uživatel měl při použití vědět, časová a paměťová složitost, příklady použití, jaké výjimky může funkce vyhodit atd. Vše, co je potřeba pro to, aby uživatel mohl funkci používat bez starostí o to, že si není jistý, co funkce vlastně dělá.
  • Testovatelný kód. Váš kód by měl být testovatelný. Měli byste jej navrhovat a psát s ohledem na to, aby jej bylo možné otestovat. S tím souvisí i znalost toho, co k testovatelnému kódu přispívá a co naopak ne. Např. pokud je váš kód zamořen Singletony, tak budete mít s testováním pravděpodobně problém.
  • Kód pokrytý testy. Kód by měl být pokrytý jednotkovými testy a dalšími druhy testů, aby šlo i po změnách ověřit, že stále vše funguje. Měli byste testovat nejen reakce na korektní vstupy, ale i reakce na nekorektní vstupy (výjimky, asserty apod.). Pokud je to potřeba, tak psát i testy na časovou náročnost, paměťovou náročnost apod.
  • Udržovatelnost. Kód by měl být otestovaný, vhodně strukturovaný a rozdělený do modulů a balíčků. Pokud je potřeba něco změnit, tak by mělo být možné to změnit na jediném místě bez toho, abys se musel měnit kód na tisíci dalších místech. Každý balíček/modul/třída/funkce by měl mít jednu odpovědnost.
  • Rozšiřovatelnost. Kód by měl být snadno rozšířitelný. Při přidání nové funkcionality by nemělo být třeba měnit kód na mnoha místech.
  • Čitelnost a pochopitelnost. Kód by měl být čitelný a pochopitelný. Měl by využívat vhodná pojmenovaní, konsistentní odsazení apod. Mělo by být jasné, co dělá. V ideálním případě by měl být kód samopopisný, aby nebylo potřeba komentářů. Komentáře totiž časem za kódem začnou zastarávat. Pokud už jsou v kódu komentáře, tak by měly popisovat důvody, proč kód něco dělá tak, jak dělá, nikoliv jen opisovat to, co kód dělá. Pokud kód musí dělat něco neobvyklého, tak by měl být doplněn detailní komentář, proč bylo zvoleno toto řešení.
  • Odolnost vůči změnám. Když se změní okolní kód, tak by váš kód měl stále fungovat. Křehký kód, který rozbije sebemenší vnější změna, bude můrou pro programátory, kteří jej používají. Taktéž bude výsledek náchylný k chybám. Váš kód by měl být proto robustní, aby se se změnami vypořádal.
  • Důsledné opravy chyb. Pokud opravujete chybu, měli byste důkladně analyzovat, proč k ní došlo a co jí způsobuje. Nikoliv udělat první hotfix, který vás napadne a u kterého se bude zdát, že funguje. Před opravením byste se měli pokusit chybu reprodukovat, ideálně za pomocí jednotkových testů, abyste se ujistili, že jste chybu opravili. Pokud si nepřidáte test, který chybu simuluje, tak riskujete, že teď ji sice opravíte, ale po několika změnách v kódu ji tam zase zanesete. Opravami chyb pak strávíte více času, než vývojem.
  • Znalost programovacího jazyka. Měli byste velmi detailně znát jazyk, ve kterém programujete. Pokud si nejste jistí, jak se některá konstrukce chová, zjistěte si to. Jinak riskujete, že se bude chovat jinak, než očekáváte, což povede k chybám.
  • Znalost standardních knihoven. Abyste znovu nevynalézali kolo, měli byste se detailně seznámit se standardní knihovnou jazyka, ve kterém programujete. Standardní knihovny bývají odladěné a otestované, takže je menší pravděpodobnost výskytu chyby, než když si na koleně vytvoříte vlastní řešení.
  • Detailní znalost méně známých záležitostí. Pokud pracujete s čísly s plovoucí řádovou čárkou, měli byste chápat jejich specifika, limitace a chování. Pokud zpracováváte vstup ve znakové sadě Unicode (resp. v některém z kódování), měli byste se s tímto standardem dobře seznámit, aby váš kód korektně reagoval na možné vstupy. Např. ň je možné zapsat jako jeden znak, nebo jako dvojici znaků n a ˇ. To stejné platí o dalších oblastech. Celkově byste měli znát normy a standardy, které jsou pro váš kód relevantní.
  • Pochopení kódu. Pokud kopírujete nějaký kód z Internetu, vždy byste jej měli nejdříve pochopit. Žádný bezmyšlenkový copy&paste. Ten vede na kód náchylný k chybám. Pokud plně nechápete to, s čím pracujete, pak nemůžete vytvářet absolutně korektní kód.
  • Vyvarování se TODO. TODO v kódu je "F*ck you!" na lidi, kteří kód po vás převezmou a budou spravovat. Pokud už musíte TODO použít, dokonale tam popište, co je za problém, co je potřeba vyřešit a proč jste to nevyřešili. To se také týče dokumentace, např. popisu funkcí. Zkuste se vžít do uživatele, který si není jistý, co jistá funkce dělá. Podívá se do dokumentace a tam uvidí jen TODO... Já si vždycky představím, že mi tímto programátor vzkazuje "F*ck you!".
  • Kontrola chybových stavů. Za všech situací byste měli detekovat a reagovat na chybové stavy. Pokud váš jazyk nepodporuje výjimky, musíte kontrolovat návratové hodnoty či jiné indikátory. Pokud podporuje, měli byste všechny možné výjimky odchytávat a reagovat na ně.
  • Kontrola vstupu. Před zpracováním vstupu byste měli verifikovat, že je korektní. U funkcí byste měli mít popsané a kontrolované preconditions. Nekontrolování vstupu vede na zneužitelný kód (shellcode, SQL injection, buffer overflow atd.).
  • Kontrola integrity. V průběhu vykonávání vašeho kódu byste měli kontrolovat invarianty (co musí stále platit). Před návratem výsledku byste měli zkontrolovat, zda výsledek odpovídá tomu, co se od funkce očekává (postconditions). Měli byste dávat záruky s ohledem na bezpečnost kódu využívající výjimky (tzv. exception safety).
  • Shovívavost ke vstupům, ale nekompromisnost k výstupům. Aby váš kód byl robustní, měli byste podporovat různorodé vstupy. Výstup, který váš kód produkuje, by se ale měl striktně držet toho, co je v jeho popisu.
  • Nestačí, že to vypadá, že kód funguje. Kód by měl fungovat. Tečka. Nestačí, že jej odzkoušíte na pár příkladech a ve všech těchto případech funguje. Pokud implementujete zpracování dat dle nějakého standardu, měli byste se s tímto standardem dokonale seznámit a kód implementovat tak, aby fungoval pro všechny možné vstupy. Do vaší testovací sady byste měli zařadit i okrajové případy, které vy sice nepoužíváte, ale standard je umožňuje. Pokud byste provedli implementaci jen na základě toho, jaké máte vstupy, tak váš kód nebude robustní a brzy dojde k tomu, že se najde vstup, na kterém váš kód selže.
  • Podchycení speciálních případů. Nestačí, že kód funguje u běžných případů. Měl by se korektně chovat i u speciálních případů, které běžně nenastávají. Až totiž nastanou, tak může být na opravu pozdě.
  • Časová a paměťová náročnost. Měli byste si dát pozor, abyste nepoužívali algoritmy či datové struktury, které se v případě rostoucí velikosti vstupu budou chovat neadekvátně. Pokud to bude možné, měli byste používat algoritmy, které mají garantovanou délku zpracování.
  • Kód dělá přesně to, co má. Nic méně a nic více.

Že to dá zabrat? Ano, dá. Ale výsledek stojí za to.

Co programátorům brání v psaní vysoce kvalitního kódu?

  • Nezkušenost. Vysoce kvalitní kód lze psát jen s dostatečnou praxí a zkušenostmi. Snažte se nabrat co nejvíce zkušeností. Párujte se zkušenějšími programátory, ale i s méně zkušenými. Člověk si až při vysvětlování látky někomu méně znalému ověří, zda tématice skutečně rozumí, nebo se jedná jen o povrchní znalost.
  • Lenost. Musíte být schopni si poručit.
  • Nezájem. Někdo o to prostě nemá zájem. Odsedí si svých osm hodin v kanclu a odejde domů s výplatou.
  • Neochota se učit. Je potřeba se neustále vzdělávat. Být programátorem znamená studovat a učit se celý život. Nefunguje to tak, že se něco naučíte a pak se již věnujete programování bez toho, abyste se učili nové věci. Čtěte cizí kód zkušených programátorů a snažte se z něj poučit. Čtěte knihy a články zabývající se vývojem a souvisejícími činnostmi. Udržujte si rozhled.
  • Nepochopení. Je potřeba pochopit, proč je psaní vysoce kvalitního a korektního kódu důležité. Musíte to chápat celým svým srdcem a být o tom skálopevně přesvědčeni. Někdy je jediná možnost se spálit. Potom člověk začne uvažovat jinak.
  • Nedůslednost. Pro psaní kvalitního kódu je důležitá důslednost. Stačí jedno malé opomenutí a bezpečností chyba je na světě.
  • Zaměstnavatelé či kolegové. Pokud váš zaměstnavatel či kolegové nechápou, proč psát kvalitní kód, zkuste jim pomoci. Vysvětlete jim, co tím získají. Pokud váš šéf i přesto nechce, abyste psal kvalitní kód a zajímá ho jen to, že kód vypadá, že funguje, běžte jinam, kde o to budou mít zájem. Nenechte je, aby z vás vysáli vaši vášeň.

Komentáře

Ahoj Petře,

díky za parádní článek. O tom jak psát rozhraní knihoven a knihovny samotné napsal pěkný článek Armin Ronacher.

Co se týče té bezpečnosti, tak bych ještě dodal, že před uvolněním alokované paměti by měl být její obsah skartován (např. pomocí memset), aby po skončení aplikace nemohla jiná aplikace alokací a následným zkoumáním obsahu alokované paměti získat případná citlivá data. Např. v MS Windows jsem se při ladění pomocí debuggeru nestačil divit co vše bylo na heapu.

Díky za zajímavý odkaz.

Co se týče skartování obsahu paměti, tak na Linuxu to funguje z bezpečnostních důvodů tak, že když proces dostane přidělenou stránku od jádra, která dříve patřila jinému procesu (čti "fyzická paměť pro danou stránku obsahovala data od jiného procesu"), tak jádro obsah této stránky automaticky vynuluje. Samozřejmě, toto je systémově specifické a na některých Linuxových systémech s omezenými prostředky (např. vestavěná zařízení) může být toto nulování z výkonnostních důvodů vypnuto.

Přidat komentář