Skip to content

Latest commit

 

History

History
96 lines (56 loc) · 11.7 KB

03-before-start.md

File metadata and controls

96 lines (56 loc) · 11.7 KB

Прежде, чем начать

Чтобы рефакторинг прошёл быстро, без регрессий и не нагружая команду большим количеством работы, перед его началом код стоит исследовать и подготовить к изменениям. А именно:

  • Определить границы рефакторинга.
  • Покрыть выбранную часть кода тестами.
  • Настроить линтеры и компилятор.

В этой главе обсудим, как упростить подготовку кода и зачем нужен каждый из перечисленных пунктов.

Определить границы

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

Такое ограничение важно по двум причинам:

  • Мы хотим оставаться в рамках временного и ресурсного бюджета, который у нас есть.
  • За маленькими изменениями проще следить и понимать, что именно сломало работу приложения.

Наметить границы в коде бывает сложно, особенно, если модули беспорядочно переплетены друг с другом. Мне в таких случаях помогает обратить внимание на данные и зависимости, с которыми работает код.

Чем сильнее отличаются данные, с которыми работают куски кода, тем выше вероятность, что это разные «единицы смысла» — самостоятельные части программы. Место стыка этих частей и будет границей, которая ограничит распространение изменений.

К слову 🪡
Физерс в «Эффективной работе с легаси» называет такие места «швами». Я иногда буду использовать этот термин как синоним.1

Границы не дадут рефакторингу модуля превратиться в «долгострой» и помогут интегрировать изменения в основную ветку репозитория чаще.

Подробнее 🔬
Чуть подробнее о поиске и использовании границ мы поговорим в следующих главах.

Покрыть тестами

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

Выявить больше крайних случаев

Крайние случаи помогут избежать регрессий и убедиться, что мы не сломали работу кода в «экзотических» обстоятельствах. («Экзотические» баги чинить сложнее.2)

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

Определить явные и неявные входные данные

Явные входные данные — это аргументы функций или методов. Неявные — зависимости, общее или глобальное состояние, контекст работы функций и методов. Систематизация входных данных упростит составление тест-кейсов.

Конкретизировать желаемый результат

Во время рефакторинга мы не меняем функциональность, поэтому желаемый результат — это фактическое поведение программы. Зафиксировать это поведение мы можем в виде данных (например, результата работы функции) или желаемого побочного эффекта (изменения состояния или вызова API).

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

Схема общения частей кода друг с другом; одна из частей под рефакторингом, результаты изменений кода видны в местах, где эта часть соединяется с другими

Если результат находится на границе, его проще проверять

Настроить автотесты

Нам потребуется тестировать каждое изменение. Тестирование вручную будет утомлять, из-за чего мы можем начать лениться или забывать о проверке изменений.

У автоматических тестов таких проблем нет. Мы можем настроить перезапуск тестов на каждое сохранение кода и запустить их перед началом рефакторинга. Так результат проверки всегда будет перед глазами, и мы раньше заметим, какое изменение сломало работу кода.

Как выбрать вид тестов, зависит от ситуации и не так важно, как их наличие. Если можно обойтись юнит-тестами, то я предпочту использовать их. Если приходится тестировать работу нескольких модулей, может понадобиться интеграционный или E2E тест.

Основной смысл — именно в автоматизации. Чем меньше проверок мы будем делать руками, тем меньше будет вероятность человеческой ошибки.

Ужесточить настройки линтеров и компилятора

Этот пункт опциональный, но очень нравится мне лично.

Более агрессивные настройки линтера или компилятора помогают подмечать случайные ошибки и плохие практики. «Более агрессивные» настройки в моём понимании такие:

Перевести «предупреждения» в разряд «ошибок»

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

Если перевести предупреждения в разряд ошибок, то код «перестанет компилироваться». Ошибки будут принуждать «чинить» код или менять правила линтера, которые мы используем.

Однако ❗️
Не все практики, предлагаемые линтером, могут быть одинаково полезными. Мы можем выбирать правила, которые мы считаем действительно важными, и отметать другие. Главное, выбрав набор правил, следовать им без отклонений — именно с этим помогает перевод «предупреждений» в «ошибки».

Настроить больше автоматизированных правил

Если команде хочется добавить новые правила для линтера или других автоматизированных инструментов, то это отличный момент попробовать.

Но стоит помнить, что не все правила одинаково полезны и уместны в каждом проекте. Я стараюсь не идти поперёк голоса команды, и если какое-то правило линтера другие разработчики считают ненужным, я не стану его вводить.

К слову 📝
О полезных характеристиках кода, которые мне кажутся обязательными и которые при этом можно поймать линтером, мы поговорим отдельно.

Footnotes

  1. “Working Effectively with Legacy Code” by Michael C. Feathers, https://www.goodreads.com/book/show/44919.Working_Effectively_with_Legacy_Code

  2. “Debug It!: Find, Repair, and Prevent Bugs in Your Code” by Paul Butcher, https://www.goodreads.com/book/show/6770868-debug-it