Недавно я смотрел «Все мелочи» из RailsConf 2014. Во время этого выступления Сэнди Метц реорганизует функцию, которая включает в себя большой вложенный оператор if:
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
Первый шаг - разбить функцию на несколько более мелких:
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
То, что я нашел интересным, было способом, которым были написаны эти меньшие функции. brie_tick
Например, был написан не путем извлечения соответствующих частей исходной tick
функции, а с нуля, ссылаясь на test_brie_*
модульные тесты. Как только все эти юнит-тесты пройдены, brie_tick
считается выполненным. Как только все маленькие функции были выполнены, исходная монолитная tick
функция была удалена.
К сожалению, докладчик, казалось, не знал, что этот подход привел к тому, что три из четырех *_tick
функций были неправильными (а другая была пустой!). Существуют крайние случаи, в которых поведение *_tick
функций отличается от поведения исходной tick
функции. Например, @days_remaining <= 0
в brie_tick
должно быть < 0
- так brie_tick
не работает правильно при вызове с days_remaining == 1
и quality < 50
.
Что здесь пошло не так? Это провал тестирования - потому что не было тестов для этих конкретных крайних случаев? Или провал рефакторинга - потому что код должен был быть преобразован шаг за шагом, а не переписан с нуля?