Aktuální situace ve světě je naprosto stvořená k tomu, aby na člověka padla deprese a bezmoc. Co si budeme nalhávat, stalo se to i mně. Nicméně z úplně jiného důvodu, než by člověk čekal. Za poslední měsíc jsem se totiž v míře větší než malé setkal se značně vyhraněnými názory na tzv. Self-documenting code. Buď se jednalo o pohovory s potenciálními novými kolegy nebo o internetové blogy/články, které razí teorii, že Self-documenting code je ten jediný správný přístup k psaní dokumentace, resp. nepsaní jakékoliv dokumentace.
Možná je to jen o nepochopení…
Myšlenka je to samozřejmě krásná – budeme psát takový kód, který je naprosto samovysvětlující a nepotřebuje jakoukoliv dokumentaci. Už na této úrovni se dá celkem polemizovat, ale budiž, k danému se ještě vrátíme. Horší však je, že se často argumentace (pokud se tak dá nikoliv výjimečná diskuzní zákopová válka vůbec nazvat) uchyluje k faulům typu: „pokud váš kód potřebuje dokumentaci, tak je špatný“, „pokud neumíte psát self-documenting code, tak jste špatní programátoři“ a tak dále. Nejednou jsem se také setkal s názorem, že u větších projektů dává dokumentace smysl, ale u menšího start-upu s týmem do 5 lidí je naprosto zbytečná. K pomyslnému poslednímu hřebíčku do rakve snad jen chybí, aby ještě někdo znovuobjevil 20 let starý agilní manifest a zmínil slavný bod „Working software over comprehensive documentation“. Aby nedošlo k mýlce – tento bod nikdy neříkal „nepište dokumentaci“, pouze „nepište cokoliv navíc, než je potřeba“. Že to pak mnoho týmů nepochopilo (a někteří to nechápou ani dnes), je věc jiná.
Mít dobře čitelný kód je samozřejmě naprosto správné. Nikdo z nás přece nechce číst něco takového:
x, y = Adafruit_DHT.read_retry(11, 1) if x is not None and y is not None: return x, y else: return -99, -99
Stejný kód, nicméně násobně pochopitelnější, může vypadat například takto:
humidity, temperature = Adafruit_DHT.read_retry( Adafruit_DHT.DHT11, SENSOR_PIN) if humidity is not None and temperature is not None: logging.debug("Sensors read (Temp=%0.1f, Humidity=%0.1f)", temperature, humidity) return humidity, temperature else: logging.error("Error reading sensors!") return UNKNOWN_SENSOR_VALUE, UNKNOWN_SENSOR_VALUE
Takže máme vyhráno? Stačí dobře pojmenovat proměnné, strukturovat kód, upravit kus tady a tamhle a je hotovo? Rozhodně ne! U jakéhokoliv kódu bychom si měli pokládat nejenom otázku „co daný kód dělá“, ale také jestli „dělá to, co opravdu dělat měl“. Zkusme naprosto triviální příklad algoritmu, který otestuje, zda je, či není, daný řetězec palindromem. Tentokrát použijeme jazyk Java:
public static boolean isPalindrome(String word) { if(word == null) { throw new IllegalArgumentException("Not a palindrome"); } for(int i=0; i < word.length() / 2; i++) { if(word.charAt(i) != word.charAt(word.length() - i - 1)) { return false; } } return true; }
Je tento kód čitelný? Určitě ano. Rozumíme, co tento kód dělá? Ano, to je naprosto zřejmé. Algoritmus nejdříve otestuje, zda je vstup validní, následně iteruje do poloviny řetězce (jelikož se zaokrouhluje celočíselně vždy dolů, nemusíme řešit sudou/lichou délku). Pokud narazí na jakoukoliv neshodu, výsledek funkce je negativní. Pokud algoritmus doiteruje do konce, výsledek funkce je pozitivní. Kde je tedy problém? Problémů je hned několik. V první řadě si můžeme položit otázku, zda má kód opravdu vyhazovat výjimku při nevalidním vstupu. Bylo to tak chtěné? Kód dále neřeší velikost znaků řetězce – je to tak správně? Znamená to, že řetězec „ELE“ palindromem je, ale „Ele“ již nikoliv? Je to korektní? Co když bude ve vstupu znak mezery? Co když budou v řetězci jiné znaky než písmena? A takto můžeme pokračovat dál a dál.
Kód je napsaný správně a jeho čtenář přesně ví, co dělat má a co dělá. Co když ale nedělá to, co by dělat měl, tedy to, k jakému má sloužit účelu? Co kdyby zadání znělo:
„Funkce vrací pozitivní výsledek za předpokladu, že se vstupní slovo čte stejně od začátku jako od konce. Ve všech ostatních případech (včetně chybného vstupu) vrací funkce negativní výsledek.“
Takové zadání má opravdu daleko k dokonalosti, nicméně nám minimálně odpovídá na problematiku malých/velkých písmen (slovo se tak má číst, čili velikost písmen je irelevantní), a také na to, zda vyhazovat výjimku (zadání jednoznačně říká, že se má v takovém případě vracet negativní výsledek). Zde lze namítnout, že stačí upravit kód a není potřeba dokumentace. Pak se ale musíme zatočit v kruhu, jelikož se nabídne otázka, proč aplikace vrací negativní hodnotu pro chybný vstup (chyba s null typicky ukazuje na programátorskou chybu) a nevyhazuje výjimku.
Pár rad do vývojářského života
Otázka detailu dokumentace často končí filosofickými debatami, které popravdě nemají vítěze. Do takové diskuze se pak může hodit níže uvedená trojice pravidel.
#1 Každý veřejný kód může být něčí budoucí API
Je to tak! Nezáleží, jestli jste v týmu sami, nebo je vás 100. Pokud je cokoliv ve vašem kódu k dispozici ostatním (typicky s public viditelností), tak se jedná o potenciálního kandidáta na API, a tedy si daná část zaslouží dokumentaci. Argument, že pak musíte udržovat kód i dokumentaci, a tedy vzniká potenciální zdroj chyb, rozhodně neobstojí. Programátorská dokumentace je součást kódu – a tedy je buď dobře, nebo špatně (od toho jsou revize kódu atd.).
Představte si libovolnou rozšířenou, populární nebo obecně uznávanou knihovnu (například Spring/SpringBoot, Lombok, Material-UI, Bootstrap apod.). Zde se dá předpokládat, že pokud je něco takto populární, a tedy „dobrého“, nepíší to nejspíše podprůměrní/špatní programátoři (například Rod Johnson). Mohli by si tedy dovolit psát Self-documenting code, nicméně oni to nedělají. Proč? Protože známka kvality, známka dobře napsaného kódu, je skrytá i v účelně napsané a uživatele knihovny podporující dokumentaci. Ti, co si to uvědomují, věnují dokumentaci odpovídající péči. Nebo vy sami si místo dobře zdokumentované knihovny s množstvím ukázek a příkladů použití vybíráte nezdokumentované kusy kódu?
#2 Věříte, že reverse-engineering cizího kódu je rychlejší než čtení dokumentace?
Dovolím si ukázku přímo z JDK, konkrétně hojně využívanou metodu:
- public String substring(int beginIndex, int endIndex)
Co může být jednoduššího, že? Nicméně hned při prvním použití vás napadne, zda je koncový index exklusivní, nebo inkluzivní, což je docela zásadní. Dokumentace v tomto případě hovoří jasně: „Returns a new string that is a substring of this string. The substring begins at the specified beginIndex and extends to the character at index endIndex – 1. Thus the length of the substring is endIndex-beginIndex.“
Naprosto jasné. Lze polemizovat, proč se druhý parametr nejmenuje třeba „endIndexExclusive“. Dokumentace ale uvádí i názorné příklady, co funkce vrátí pro jaký vstup. Nyní si představte, že dokumentaci mít nebudete. Pojďme tedy využít zdrojové kódy:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); }
Kód je čitelný, je zřejmé, co dělá. Nicméně opravdu je zřejmé, že je koncový index exklusivní? Nikoliv na 100 %. Pro naprostou jistotu je nutné pokračovat do interního konstruktoru, který standardně třída String neumožňuje veřejně použít. Není dokumentace rychlejší?
Zde by mělo platit pravidlo, že pokud chcete využít cizí kód, analyzujete jeho vhodnost pro vaše použití nejdříve bez dokumentace (název funkce, signatura funkce, názvy parametrů). Pokud si nejste jisti, pokračujete dostupnou dokumentací funkce (například JavaDoc), pokud dané také nic neříká, teprve pak pokračujete do kódu. Někdy je nutné i při přítomnosti dokumentace kód analyzovat, ale ruku na srdce – pokud si můžete v klidu něco přečíst bez nutnosti zkoumání kódu, je to typicky rychlejší.
#3 Dokumentace je dlouhodobá, vaše paměť nikoliv
Možná víte o vašem programu vše, možná to ví i celý váš tým. Naše výstupy ale typicky tráví v provozu násobně více času než ve vývoji. To je realita údržby – i při agilním vývoji a dlouhodobém rozvoji budou části, na které budete sahat neustále, a naopak budou i části, které se aktualizují jednou za několik měsíců. Bude ale váš tým stejný? Opravdu si budete vše pamatovat? Opravdu budete chtít celou historii novým kolegům přeříkávat, anebo je budete moci odkázat na dokumentaci? A co když svůj kód otevřete vy sami za 6 měsíců nebo dva roky – vybavíte si opravdu všechno?
Nikdo nemluví o desítkách stran dokumentace, ale existuje velké množství přístupů, jak dokumentaci efektivně udržovat. Například pro dokumentaci (neustále se měnící) architektury lze krásně použít tzv. Architecture Decision Records. Tomuto blog postu bude příští rok 10 let, dané téma již řadu let adresují i kapacity jako Martin Fowler a jiní. Z dnešního pohledu se tedy dá říci, že se jedná o starý koncept – nicméně realitou je, že jej nevyužívá ani zdaleka tolik týmů, kolik by mělo. Proč tomu tak je, je mi záhadou.
Co je to tedy Self-documenting code?
V některých diskuzích a článcích se můžete taktéž dozvědět, že Self-documenting code je absolutní mýtus. Tohle je však dle mého názoru jen opačný extrém, se kterým nelze souhlasit. Dobře napsaný kód, který je čitelný a jasně sděluje svoji funkcionalitu a účel, by měl být cílem každého vývojáře. Důležitým aspektem v tomto bodě však je, že to samo o sobě nestačí. Je rozdíl mezi dobře napsaným a samovysvětlujícím fragmentem kódu a knihovnou, aplikací, systémem, platformou atd. Každá úroveň abstrakce si žádá to své, nicméně věřím, že pro většinu z nás by měl být kvalitní kód vždy spojen také s kvalitní dokumentací.
Autor: Michal Petřík