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?