Blíží se Vánoce, čas stresu a shonu za nejdražšími Vánočními
dárky, pro naše nejdražší. Čas, kdy se ve velkém mordují kapři (navíc
na veřejnosti, no fůj!) a kácí se stromky. Ano, ty které potom ověsíme
nejrůznějšími koulemi a řetězy, aby pak několik týdnů opadávalo
jehličí a vůbec. O tomhle ale psát nechci. :)
Dneska se podíváme na jiný úkaz. A to na problematiku zanořování
kódu, který ve výsledku vypadá jako stromečky. (Pokud jste takový kód
ještě neviděli, což pochybuju, tak ukázku najdete třeba tady v sekci Dark
Magic.)
V čem je problém?
Možná si říkáte, „co to ten Roubíček zase vymejšlí? Vždyť
takovej kód je přece v pořádku,“ (tj. dělá, to, co má.) „A navíc
nemusíme chodit do lesa, protože máme stromečky pořád na očích!“ To je
fakt. Jenže, ruku na srdce, dokážete odhadnout na první pohled, co takový
kód vlastně dělá? Moc asi ne. A proč? Protože pět
úrovní zanoření (nepočítám namespace, class ani metodu) je už
trochu moc.
Jak moc? No hrozně moc. Fakt! Ono se to totiž dá krásně změřit, např.
pomocí metriky Cyclomatic
complexity.
Když se podíváme na tuto metriku u ukázkového příkladu, zjistíme,
že metoda GetRouteData dosahuje hodnoty 17. Co to
znamená? Když si přečtete odkazovaný popis metriky, dozvíte se také, že
její hodnota určuje míru možného výskytu chyb. Čím větší je
cyklomatická komplexita, tím více toho modul dělá a zvyšuje se riziko
přehlédnutých chyb. Doporučuje se mít komplexitu do deseti a tady tuto
hodnotu překračujeme takřka dvojnásobně.
It smells like refactoring time!
Refaktorovat, refaktorovat,
refaktorovat
Jednoduše jsme pomocí metriky odhalili kandidáta, kde budeme kácet
stromy. Když se podíváme pozorně na ukázkový příklad, můžeme si
všimnout bloků kódu, které začínají komentářem. Budu se opakovat, ale
nepište zbytečný kód!
Tohle jsou jasné samostatné metody. Použijeme tedy refaktoring Extract
method. Po extrahování čtyř metod, už se nám metoda vejde na jednu
obrazovku a cyklomatická komplexita spadne na trojku. Takřka na jednu
šestinu!
Huh. A to je teprve začátek.
Po prvním kole vypadá výsledek asi tak:
public override RouteData GetRouteData(HttpContextBase httpContext) {
domainRegex = CreateRegex(Domain);
pathRegex = CreateRegex(Url);
string requestDomain;
string requestPath;
RequestInformation(httpContext, out requestDomain, out requestPath);
Match domainMatch = domainRegex.Match(requestDomain);
Match pathMatch = pathRegex.Match(requestPath);
RouteData data = null;
if (domainMatch.Success && pathMatch.Success) {
data = new RouteData(this, RouteHandler);
AddDefaultsFirst(data);
IterateMatchingDomainGroups(domainMatch, data);
IterateMatchingPathGroups(pathMatch, data);
}
return data;
}
Není to žádná nádhera, ale je mnohem přehlednější, než originál.
A v řeči čísel:
Rozplétáme podmínky
Tak, snížili jsme komplexitu metody, ale tiše nám narostla komplexita
třídy. Klidně můžeme pokračovat v cestě, to zvládneme později. Teď se
zaměříme na ty velký šestky. Na první pohled cyklus se spoustou
zanořených podmínek. Pryč s nimi! Teď přijde na řadu refaktoring
Flatten Conditional a z vše objímajícího
ifu uděláme jen guarda. Dále tu mám dva zanořený
ify? Proč, když je můžeme logicky spojit operátorem
&&? Směle do toho! Pro změnu použijeme refaktoring
Combine Conditionals a vytvoříme jednu dlouhou a ošklivou
podmínku…
Čas na refaktoring Extract method a podmínku pěkně
smysluplně pojmenovat. Po pár magických kombinacích kláves se nám
z týhle zrůdičky:
private static void IterateMatchingPathGroups(Match pathMatch, RouteData data) {
for (int i = 1; i < pathMatch.Groups.Count; i++) {
Group group = pathMatch.Groups[i];
if (group.Success) {
string key = pathRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0)) {
if (!string.IsNullOrEmpty(group.Value)) {
data.Values[key] = group.Value;
}
}
}
}
}
Stane opět poměrně snadno pochopitelný kód:
private static void IterateMatchingPathGroups(Match pathMatch, RouteData data) {
for (int i = 1; i < pathMatch.Groups.Count; i++) {
Group group = pathMatch.Groups[i];
if (IsNotMatch(group))
continue;
string key = pathRegex.GroupNameFromNumber(i);
if (IsInvalidKeyOrValue(group, key))
continue;
data.Values[key] = group.Value;
}
}
Navíc, když se teď na ten kód podíváme, zjistíme, že se ty původní
šestky (z kterých už jsou čtyřky) liší jen v tom, že používají
každá jinou členskou proměnou, naštěstí stejného typu. Tak jdeme zase
refaktorovat. :) Nejprve si tedy tuto proměnou uložíme lokálně na začátku
metody a ze zbytku extrahujeme metodu novou s jedním parametrem navíc.
U druhý čtyřky už to dělat nemusíme, páč výsledný extrakt z té
první je plně vyhovující našim potřebám a tak ho zavoláme jen s jiným
parametrem. Ze dvou čtyřek máme jednu a dvě jedničky. A komplexita třídy
se nám vrátila k původní hodnotě. Ano, máme tu teď spoustu malých
metod, které ale mají jasný účel. A když se podíváme blíž, zjistíme,
že můžeme úplně v klidu některé z nich extrahovat no úplně nové
třídy. Ale to už je na jinou pohádku.
Stromečky jsou totiž fuč. :)
PS. Zmíněné refaktoringy a vizualizace metrik jsou součásti skvělého
produktu Refactor
Pro! nebo samotné refactoringy v neplacené verzi Code
Rush Xpress.