Чтобы рефакторинг прошёл быстро, без регрессий и не нагружая команду большим количеством работы, перед его началом код стоит исследовать и подготовить к изменениям. А именно:
- Определить границы рефакторинга.
- Покрыть выбранную часть кода тестами.
- Настроить линтеры и компилятор.
В этой главе обсудим, как упростить подготовку кода и зачем нужен каждый из перечисленных пунктов.
Под границами рефакторинга мы будем понимать места стыка между кодом, который мы будем менять, и всем остальным. Они определяют, в каком месте наши изменения должны остановиться, то есть какой код мы менять не будем.
Такое ограничение важно по двум причинам:
- Мы хотим оставаться в рамках временного и ресурсного бюджета, который у нас есть.
- За маленькими изменениями проще следить и понимать, что именно сломало работу приложения.
Наметить границы в коде бывает сложно, особенно, если модули беспорядочно переплетены друг с другом. Мне в таких случаях помогает обратить внимание на данные и зависимости, с которыми работает код.
Чем сильнее отличаются данные, с которыми работают куски кода, тем выше вероятность, что это разные «единицы смысла» — самостоятельные части программы. Место стыка этих частей и будет границей, которая ограничит распространение изменений.
К слову 🪡 |
---|
Физерс в «Эффективной работе с легаси» называет такие места «швами». Я иногда буду использовать этот термин как синоним.1 |
Границы не дадут рефакторингу модуля превратиться в «долгострой» и помогут интегрировать изменения в основную ветку репозитория чаще.
Подробнее 🔬 |
---|
Чуть подробнее о поиске и использовании границ мы поговорим в следующих главах. |
Код внутри выделенных границ нужно покрыть тестами. С их помощью мы будем проверять, что ничего не сломали во время рефакторинга. Чтобы тесты приносили больше пользы, я стараюсь выполнить несколько условий:
Крайние случаи помогут избежать регрессий и убедиться, что мы не сломали работу кода в «экзотических» обстоятельствах. («Экзотические» баги чинить сложнее.2)
Чем крайние случаи разнообразнее, тем легче подобрать и систематизировать тестовые данные для различных ситуаций. Знания о поведении приложения в этих ситуациях пригодится нам и в будущем, после рефакторинга.
Явные входные данные — это аргументы функций или методов. Неявные — зависимости, общее или глобальное состояние, контекст работы функций и методов. Систематизация входных данных упростит составление тест-кейсов.
Во время рефакторинга мы не меняем функциональность, поэтому желаемый результат — это фактическое поведение программы. Зафиксировать это поведение мы можем в виде данных (например, результата работы функции) или желаемого побочного эффекта (изменения состояния или вызова API).
В идеале результат должен находиться на границе части кода, которую мы рефакторим. Проверять такой результат проще, а количество затронутого кода будет минимально.
Если результат находится на границе, его проще проверятьНам потребуется тестировать каждое изменение. Тестирование вручную будет утомлять, из-за чего мы можем начать лениться или забывать о проверке изменений.
У автоматических тестов таких проблем нет. Мы можем настроить перезапуск тестов на каждое сохранение кода и запустить их перед началом рефакторинга. Так результат проверки всегда будет перед глазами, и мы раньше заметим, какое изменение сломало работу кода.
Как выбрать вид тестов, зависит от ситуации и не так важно, как их наличие. Если можно обойтись юнит-тестами, то я предпочту использовать их. Если приходится тестировать работу нескольких модулей, может понадобиться интеграционный или E2E тест.
Основной смысл — именно в автоматизации. Чем меньше проверок мы будем делать руками, тем меньше будет вероятность человеческой ошибки.
Этот пункт опциональный, но очень нравится мне лично.
Более агрессивные настройки линтера или компилятора помогают подмечать случайные ошибки и плохие практики. «Более агрессивные» настройки в моём понимании такие:
Предупреждения линтера — это коллективный опыт индустрии, который может оказаться ценным. Однако предупреждения легко пропустить, потому что они не заставляют сборку кода «падать с ошибкой».
Если перевести предупреждения в разряд ошибок, то код «перестанет компилироваться». Ошибки будут принуждать «чинить» код или менять правила линтера, которые мы используем.
Однако ❗️ |
---|
Не все практики, предлагаемые линтером, могут быть одинаково полезными. Мы можем выбирать правила, которые мы считаем действительно важными, и отметать другие. Главное, выбрав набор правил, следовать им без отклонений — именно с этим помогает перевод «предупреждений» в «ошибки». |
Если команде хочется добавить новые правила для линтера или других автоматизированных инструментов, то это отличный момент попробовать.
Но стоит помнить, что не все правила одинаково полезны и уместны в каждом проекте. Я стараюсь не идти поперёк голоса команды, и если какое-то правило линтера другие разработчики считают ненужным, я не стану его вводить.
К слову 📝 |
---|
О полезных характеристиках кода, которые мне кажутся обязательными и которые при этом можно поймать линтером, мы поговорим отдельно. |
Footnotes
-
“Working Effectively with Legacy Code” by Michael C. Feathers, https://www.goodreads.com/book/show/44919.Working_Effectively_with_Legacy_Code ↩
-
“Debug It!: Find, Repair, and Prevent Bugs in Your Code” by Paul Butcher, https://www.goodreads.com/book/show/6770868-debug-it ↩