После того, как мы обозначили границы изменений, исследовали код и покрыли его тестами, мы можем приступить к рефакторингу.
Во время работы нам хочется, чтобы изменения кода были максимально полезными и при этом находились внутри обозначенных границ. В этой главе обсудим эвристики, которые помогают это делать.
Маленький шаг — это минимальное изменение, несущее смысл. Хороший пример маленького шага — это атомарный коммит (Atomic Commit). Такой коммит содержит осмысленное изменение функциональности и переводит кодовую базу из одного рабочего состояния в другое рабочее состояние.1 С их помощью мы можем развивать и менять кодовую базу, держа её при этом валидной в каждый момент времени.
К слову 💡 |
---|
Атомарные коммиты позволяют в будущем проводить бисекцию репозитория во время поиска багов.23 Когда код валиден в каждом из коммитов, мы можем «путешествовать во времени» по репозиторию, переключаясь между разными коммитами. |
Размер коммитов зависит от привычек и предпочтений команды. Лично я придерживаюсь правила «чем меньше коммит — тем лучше». Например, в своих проектах я коммичу отдельно даже переименование методов и переменных.
Маленькие шаги побуждают декомпозировать задачи на более простые. Так неподъёмные фичи превращаются в наборы компактных задач, которые не так страшно чаще сливать в основную ветку репозитория. Без декомпозиции такая задача может превратиться в долгострой, блокирующий всю команду.
Подробнее 🔬 |
---|
Суть частой интеграции в основную ветку (Continuous Integration) в том, чтобы команда как можно чаще синхронизировала изменения в коде между собой. О пользе подхода хорошо написали Марк Симанн в “Code That Fits in Your Head” и Скотт Бернштайн в “Beyond Legacy Code”.45 |
Блокирующих задач лучше избегать в принципе, а при рефакторинге кода особенно. Если вся команда занята рефакторингом, это может дорого стоить. Прецедент дороговизны может в будущем лишить проект ресурсов на рефакторинг вообще.
Кроме этого маленькие шаги позволяют в любой момент «отложить» рефакторинг и переключиться на другую задачу. Если мы работаем с git, то можем использовать «полочки», чтобы сохранять недоделанную работу через git stash
. Так процесс разработки будет более гибким, и мы сможем быстрее реагировать на «более срочные задачи» типа внезапных багов на продакшене.
Также результаты маленького шага проще окинуть взглядом и проверить перед тем, как сделать с ними коммит. Такие проверки помогают отсеивать изменения, не относящиеся к текущей задаче. Например, случайные изменения, вызванные автоматическими инструментами типа форматеров или линтеров.
И напоследок, маленькие шаги проще описывать в сообщениях к коммитам. Скоуп таких изменений укладывается в одно-два предложения, поэтому легче описать их смысл коротко и точно.
Этот раздел вытекает из предыдущего, но я хочу заострить на нём внимание отдельно. Проблема больших пул-реквестов в том, что...
❗️ Никто не проверяет большие и непонятные пул-реквесты
Если мы хотим улучшить код, то нам нужно, чтобы пул-реквест проверили на ревью, тогда наша задача — облегчить работу ревьюерам. Для этого мы можем:
- Ограничить размер изменений — на компактные пул-реквесты проще выделить время и вникнуть в их суть. Ревью не будет выглядеть большой внезапной работой.
- Описать контекст задачи в сообщении к PR. Причины, цель и ограничения задачи помогут поделиться с ревьюерами тем, что известно нам, но ещё не известно им. Так мы сможем предугадать вероятные вопросы и заранее ответить на них — это ускорит ревью.
К слову 📚 |
---|
Пул-реквест — это часть коммуникации в команде. Подробнее о том, как упростить коммуникацию писал Максим Ильяхов в «Новых правилах деловой переписки».6 |
Стремление к маленьким, но подробным PR помогает дробить большие задачи на задачи поменьше и продвигать рефакторинг маленькими шагами.
Чтобы код эволюционировал через валидные состояния, мы будем проверять тестами каждое изменение, каким бы маленьким оно ни было.
При использовании юнит-тестов можно держать окно с запущенными тестами рядом с редактором. При использовании более долгих тестов (например, E2E) можно запускать их перед коммитом (например, на pre-commit хуке), чтобы в репозиторий не попадал невалидный код.
Тесты должны побуждать нас коммитить в репозиторий только валидный код. Тогда в каждом коммите будет находиться набор законченных и осмысленных изменений.
К слову 🧪 |
---|
Это может работать только в том случае, когда мы доверяем тестам. Поэтому в прошлой главе мы и делали акцент на эдж-кейсах и конкретизации результата — они помогают сделать тесты более надёжными. |
Частые коммиты фиксируют опорные точки в эволюции кода. Чем такие точки чаще, тем компактнее изменения между ними. Это, например, полезно при осмотре изменений с последнего коммита, которые мы только что внесли. Компактные изменения быстрее изучить, проще понять и не так жалко откатывать.
Во время рефакторинга не всегда очевидно, как делать изменения компактнее и чаще. Мне в этом помогает правило:
❗️ Не смешивать разные техники рефакторинга в одном коммите
Это правило помогает коммитить ритмичнее и чаще: переименовали функцию — коммит, вынесли переменную — коммит, добавили код для будущей замены — коммит, и так далее.
Пока мы не смешиваем разные техники в одном коммите, нам проще отслеживать изменения кода по диффам и находить ошибки типа конфликтов имён.
Сложные техники можно разбивать на отдельные этапы, каждый из которых оформлять в виде коммита. В этом случае этапы важно выделять так, чтобы каждый из них тоже оставлял код в валидном состоянии.
К слову 👀 |
---|
Сочетание атомарных коммитов, непрерывной интеграции кода и предкоммитных проверок изменений ещё называют «тактической» работой с гитом.7 Мы тоже иногда будем использовать этот термин, говоря о таком применении гита в следующих главах. |
Во время рефакторинга мы можем найти кусок кода, который работает неправильно. Может возникнуть желание «исправить это по пути», но багфиксы и новые фичи к рефакторингу лучше не примешивать.
Рефакторинг не должен менять функциональность кода. Если мы добавили фичу во время рефакторинга, и её потребуется откатить, то нам придётся переносить конкретные коммиты или даже строчки кода руками.
Вместо этого все найденные идеи для фич лучше положить в отдельный список и вернуться к ним после рефакторинга. Если мы нашли баг, то рефакторинг стоит отложить (git stash
) и вернуться к нему после фикса. Для такой манёвренности опять же удобнее работать маленькими шагами.
Приоритет преобразований (Transformation Priority Premise, TPP) — это список действий, которые помогают развивать код от наивной простейшей реализации до более сложной, которая отвечает всем требованиям проекта.8
TPP помогает не пытаться сделать всё сразу. Он побуждает обновлять кусок кода постепенно, записывая каждый шаг в системе контроля версий, и чаще интегрировать изменения в основную ветку репозитория.
Мне лично TPP помогает не перегружать голову деталями, пока я пишу код. Все идеи для улучшений, которые возникают по ходу работы, я складываю в отдельный список. Этот список я потом сравниваю с TPP и выбираю, что реализовать следующим шагом.
Зачем 🧠 |
---|
«Выгруженные» из головы детали освобождают «оперативную память» мозга.9 Высвобожденные ресурсы можно потратить на детали задачи — это улучшает внимательность. |
Тесты и продуктовый код страхуют друг друга. Тесты проверяют, что мы не допустили ошибок в коде приложения, и наоборот. Если рефакторить их одновременно, вероятность пропустить ошибку становится выше.
Рефакторить код приложения и тесты лучше по очереди. Если во время рефакторинга приложения мы заметили, что нужно отрефакторить тест, то стоит:
- «Положить на полочку» изменения в коде приложения;
- Отрефакторить нужный тест;
- Проверить, что тест ломается по той причине, по которой должен;
- «Достать с полочки» предыдущие изменения и продолжить работать над ними.
Подробнее 🔬 |
---|
Чуть подробнее об этой технике мы поговорим в главе о рефакторинге тестового кода. |
Footnotes
-
Atomic Commit, Wikipedia https://en.wikipedia.org/wiki/Atomic_commit ↩
-
git-bisect, Use binary search to find the commit that introduced a bug, https://git-scm.com/docs/git-bisect ↩
-
“Write Better Commits, Build Better Projects” by Victoria Dye, https://github.blog/2022-06-30-write-better-commits-build-better-projects/ ↩
-
“Code That Fits in Your Head” by Mark Seemann, https://www.goodreads.com/book/show/57345272-code-that-fits-in-your-head ↩
-
“Beyond Legacy Code” by David Scott Bernstein, https://www.goodreads.com/book/show/26088456-beyond-legacy-code ↩
-
«Новые правила деловой переписки» М. Ильяхов, Л. Сарычева, https://www.goodreads.com/book/show/41070833 ↩
-
“Use Git Tactically” by Mark Seeman, https://stackoverflow.blog/2022/04/06/use-git-tactically/ ↩
-
Transformation Priority Premise, Wikipedia https://en.wikipedia.org/wiki/Transformation_Priority_Premise ↩
-
Оценка емкости рабочей памяти, Википедия, https://ru.wikipedia.org/wiki/Рабочая_память#Оценка_емкости_рабочей_памяти ↩