Безопасные шаги рефакторинга

Что это такое

Безопасный шаг рефакторинга — изменение настолько мелкое, что вы уверены в сохранении поведения, и настолько проверяемое, что можете это доказать. Доказательство даёт прогон тестов, а размер шага выбран так, чтобы при провале причиной была последняя ваша правка, а не одна из сорока. Вся процедура и состоит в том, чтобы сделать наименьшее изменение, улучшающее структуру, прогнать тесты, оставить или откатить и повторить.

Почему это важно

Альтернатива маленьким шагам — большое изменение, внутри которого нет ни одного рабочего состояния: перестроите пять вещей сразу — провалы тестов придут вместе и вперемешку, и поиск причины потребует отменить работу, которую хочется сохранить. Маленькие шаги сохраняют код готовым к выпуску в любой момент, что важно, когда рефакторинг идёт днями, а другие люди вливают изменения в те же файлы. Прочитайте сначала запись о модульном тестировании: остальная часть этой записи станет конкретнее, потому что работу делает именно страховочная сетка.

Ключевые идеи

Неизменность поведения — это инвариант

Выпишите, что входит в «поведение», прежде чем начать: возвращаемые значения, возбуждаемые ошибки, побочные эффекты и их порядок, а также публичную форму, от которой зависит другой код. Производительность — тоже поведение, как и порядок двух записей; а изменение, улучшающее структуру и задевающее что-либо из этого, не рефакторинг, что бы ни говорил комментарий к коммиту.

поведение = выходы + ошибки + побочные эффекты + их порядок
рефакторинг = меняем всё остальное

Делайте по шагу и прогоняйте тесты после каждого

Держите шаг настолько мелким, чтобы описать его одним предложением: «выделить расчёт налога в свою функцию». Прогоните тесты. Следующий шаг начинайте, только когда они зелёные. Ритм кажется медленным, но это не так: альтернатива — сеанс отладки в кодовой базе, структуру которой вы всё ещё меняете.

правка -> прогон тестов -> зелёный -> контрольная точка размером с коммит -> следующий шаг
                        -> красный -> откат, шаг мельче

Носите одну шляпу за раз

Не исправляйте ошибку и не добавляйте функцию во время рефакторинга: как только поведение меняется, прогон тестов больше не говорит, был ли ваш рефакторинг безопасным, ведь провалившийся тест может относиться к новой функции. Разделите работу: сначала отрефакторьте так, чтобы новое поведение стало легко добавить, затем добавьте его отдельным шагом. Проверяющие скажут спасибо: два вида изменений читаются в диффе совершенно по-разному.

Сначала натяните сетку вокруг непокрытого кода

Рефакторить код без тестов — гадание. Прежде чем перестраивать его, напишите несколько тестов, фиксирующих то, что он делает сегодня, включая странные случаи, которые вы подозреваете в случайности. Они нужны не для того, чтобы доказать правильность кода, а чтобы доказать его неизменность; и лишь когда они проходят на текущей реализации, у вас появляется право двигать вещи.

// характеризующий тест: фиксирует текущее поведение, верное или нет
expect(legacyFormatter(input)).toBe("odd, but current");

Знайте, когда остановиться, а когда прекратить рефакторинг

Остановитесь, когда изменение под следующее требование станет локальным и очевидным, — это и есть цель, а не красота. Рефакторинг — не право без конца перестраивать код, который никому не нужно менять, и не замена переписыванию, когда сами требования изменились до неузнаваемости. Скажите вслух, какое из двух вы делаете, прежде чем начать.

Практика

Отрефакторьте под работающим набором тестов

Возьмите функцию без тестов. Напишите три характеризующих теста, фиксирующих её текущий результат для обычного входа, пустого входа и одного крайнего случая. Затем выделите из функции одну ответственность и прогоните тесты.

Готово, когда: все три теста проходят до и после, а у выделенной функции есть имя, которое коллега может объяснить.

Откатите нарочно

Сделайте шаг рефакторинга, который считаете безопасным, затем нарочно сломайте его самым мелким способом, какой придумаете, и посмотрите, как тесты провалятся.

Готово, когда: вы увидели провал, откатили нарочную поломку и можете сказать, какой тест его поймал.

Частые ошибки

  • Рефакторинг без тестов — молчаливое изменение поведения остаётся незамеченным до продакшена; исправить: закрепите текущее поведение парой тестов заранее.
  • Смешение рефакторинга и функции в одном изменении — провалившийся тест больше не говорит, какое изменение виновато; исправить: оформите их двумя отдельными шагами.
  • Слишком крупный шаг, чтобы его описать — причина провала где-то среди последних двадцати правок; исправить: делите, пока не сможете изложить его одним предложением.
  • Переписывание вместо рефакторинга — рабочее состояние исчезает вместе с накопленным знанием; исправить: держите программу готовой к выпуску на всём протяжении.
  • Рефакторинг кода, который не меняется — усилия уходят на структуру, от которой никто не выиграет; исправить: рефакторьте там, где изменений действительно ждут.

Что читать дальше

  • Чистый код (01-cs-fundamentals/08-clean-code) — то целевое состояние, к которому вы идёте в именах, размере и структуре.
  • Модульное тестирование (01-cs-fundamentals/10-unit-testing) — страховочная сетка, от которой зависит вся эта процедура, поэтому читайте её первой.
  • Рефакторинги, которыми вы действительно воспользуетесь (01-cs-fundamentals/19-refactoring/02-refactorings-you-will-actually-use) — названные шаги, которые пора применить, раз ритм уже усвоен.