Di recente ho guardato "Tutte le piccole cose" di RailsConf 2014. Durante questo discorso, Sandi Metz ha funzione che include una grande istruzione if annidata:
def tick
if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
if @quality > 0
if @name != 'Sulfuras, Hand of Ragnaros'
@quality -= 1
end
end
else
...
end
...
end
Il primo passo è quello di suddividere la funzione in molti più piccoli:
def tick
case name
when 'Aged Brie'
return brie_tick
...
end
end
def brie_tick
@days_remaining -= 1
return if quality >= 50
@quality += 1
@quality += 1 if @days_remaining <= 0
end
Ciò che ho trovato interessante è il modo in cui sono state scritte queste funzioni più piccole. brie_tick
, ad esempio, non è stato scritto estraendo le parti rilevanti della funzione tick
originale, ma da zero facendo riferimento ai test unitari test_brie_*
. Una volta superati tutti questi test unitari, brie_tick
è stato considerato eseguito. Una volta completate tutte le piccole funzioni, la funzione monolitica tick
originale è stata eliminata.
Sfortunatamente, il presentatore sembrava inconsapevole che questo approccio portasse a tre delle quattro funzioni *_tick
sbagliate (e l'altra era vuota!). Esistono casi limite in cui il comportamento delle funzioni *_tick
differisce da quello della funzione tick
originale. Ad esempio, @days_remaining <= 0
in brie_tick
dovrebbe essere < 0
- quindi brie_tick
non funziona correttamente quando chiamato con days_remaining == 1
e quality < 50
.
Che cosa è andato storto qui? È un fallimento dei test, perché non c'erano test per questi casi particolari? O un fallimento del refactoring - perché il codice avrebbe dovuto essere trasformato passo dopo passo piuttosto che riscritto da zero?