Работа со старым кодом для многих команд является частью повседневных обязанностей. За свою карьеру я видел и применял разные способы борьбы с тяжестью легаси. Они обычно сводились к одному из трёх основных сценариев:

  1. «Работает — не трогай!»: вообще забить на чистки и ничего не менять. В некоторых случаях валидный подход. Но в коде, который приходится менять хотя бы даже эпизодически (фиксы багов, мелкие доделки, смена окружения и т. п.), со временем неизбежно приводит к катастрофе. Вам надо что‑то поменять в коде, и это оказывается невозможно сделать легко. Даже за тривиальные изменения приходится платить большой кровью.

  2. «Я прочитал Роберта Мартина»: включаем чистки в обычный код. Надеваем галстук бойскаута и чистим код прямо по ходу работы над текущими задачами. Отправляем его коллегам на ревью и ждём несколько дней, покуда они не разберутся, где заканчиваются рефакторинги и начинаются непосредственно изменения по задаче. Или же уходим по кривой дорожке рефакторингов в тёмный лес и продалбываем к чертям все изначальные сроки. Когда начинаешь приводить код к идеалу, не всегда бывает так легко остановиться!

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

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

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

Во избежания недопонимания, сразу очертим границы! Я рассматриваю ситуацию, в которой:

  • Команда развивает (или хотя бы поддерживает на плаву) определённый продукт или сервис, приносящий пользу кастомерам. Его нерационально переписывать с ноля, потому что он должен постоянно работать.

  • Ожидается, что этот продукт/сервис ещё будет жить какое‑то обозримое время (т. е., он ещё не достиг стадии end‑of‑life). Мы не можем выкинуть или заморозить код, развитие ещё продолжается.

  • У команды есть определённая степень свободы в технических решениях по этому проекту. Без этого всё написанное ниже не имеет смысла.

  • В команде существует практика peer code review. т. е., чтобы влить свой код в главную ветку, надо сперва подождать, когда его посмотрит и одобрит кто‑то из членов команды. Практика весьма распространённая, поэтому приходится под неё подстраиваться.

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

А теперь поехали!

Семь принципов

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

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

Принцип 1. Точечные правки

Суть: каждый отдельный рефакторинг должен делать ровно одну понятную вещь. Изменения, которые в нём проведены, единообразны.

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

За последнее время я пришёл к мнению, что самая подходящая метрика для оценки сложности рефакторингов — это «количество принятых решений». За каждым изменением в коде стоит то или иное решение, принятое его автором. Например:

  • «я применил подсказку из IDE, чтобы warning не мозолил глаза»

  • «я избавился от этого неиспользуемого кода, потому что в нём нет смысла»

  • «я переиспользовал тот метод, потому что он и так делает почти всё, что нужно»

  • «я разбил метод на части, так проще его читать»

  • «я применил автоформатирование, потому что привык к этому в других проектах»

  • «я переименовал эту тупую переменную, чтобы она меня не бесила»

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

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

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

Возьмём для сравнения другой инструмент: шуруповёрт. Фраза «отшуруповёртить шкаф» не нарушает никаких законов нашего языка. Вот только что это значит: собрать шкаф? разобрать? прикрепить к стене? открепить от стены? наделать дырок? Смысла такая фраза не несёт.

Точно так же и Ваш рефакторинг должен быть подчинён конкретной и понятной цели: снижение сложности, снижение размера, повышение единообразия и т. п. Не заставляйте коллег догадываться, что спрятано за Вашей фразой «отрефакторил класс XYZ». Отмечайте конкретную причину и применённое действие.

Основной профит точечного изменения в том, что его легко понять. Если изменения простые и однородные, то для его анализа не требуется включать голову на всю мощность. Даже если файлов несколько десятков! Порой достаточно и обычного «сравнения с шаблоном»: пробежаться по коду глазами и убедиться, что все изменения одинаковые. Например, если в коммите находятся только переименования из одного шаблона в другой, любое другое изменение сразу будет легко заметно.

foo -> bar
foo -> bar
foo -> meh
foo -> bar

Когда мы внедряем обычные фичи в код, то, как правило, не трогаем всю кодовую базу. В ней находятся «горячие» файлы (которые меняются чаще других), но большое число файлов остаются «холодными». Со временем, стиль кода в них начинает отставать от текущих трендов. Используя атомарные коммиты, мы можем применять одно решение сразу к большому количеству файлов, не повышая при этом радикально нагрузку на ревьювера. Становится возможным приводить код к единому стилю.

Пятна одного цвета принадлежат одному коммиту, размер пятна - объём изменения
Пятна одного цвета принадлежат одному коммиту, размер пятна - объём изменения

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

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

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

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

  • Удалили код, который, казалось бы, нигде не использовался, а он в одном случае всё же вызывался.

Такие ситуации не всегда можно угадать заранее. Если Ваше изменение атомарно, то будет проще понять причину поломки, или быстро откатиться назад через revert. А вот если в одном коммите собрана целая пачка разнотипных изменений, то это может стать серьёзной проблемой.

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

Принцип 2. Быстрые изменения

Суть: работа над отдельной чисткой не должна занимать много времени.

Во‑первых, помимо самих чисток, у нас есть обычные повседневные рабочие цели — и они должны быть более приоритетными. Во‑вторых, чем дольше мы работаем над рефакторингом, тем выше риск увлечься, закопаться и внести слишком много правок, потерять «атомарность».

Ориентир, в который я обычно стараюсь вписаться, — 15 минут на все действия по одному рефакторингу. За это время надо успеть внести сами изменения, пересмотреть их самостоятельно, проверить (компиляция + быстрые тесты), оформить коммит и отправить на ревью. Довольно жёсткий лимит, не правда ли? Поэтому надо заранее максимально чёткого понимать, что планируется сделать.

Конечно, порой я выбиваюсь из собственного ограничения. Но наличие лимита времени позволяет быстро понять, всё ли идёт по плану. Если через 20 минут после начала работы над рефакторингом у меня уже всё готово, это один расклад. А если я ещё где‑то в середине запланированного изменения, то это явный сигнал, что я попытался откусить слишком большой кусок.

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

С непривычки, удалять свой код может быть тяжело. Кажется, что в него вложен труд, и этим он ценен. Это когнитивное искажение, которое называется «ловушка невозвратных затрат». Чем больше времени и сил мы вложили в переделку кода, тем ценнее он для нас выглядит. Даже если на деле он создаёт больше новых проблем, чем решает старых.

Между тем, умение удалять собственный код может сделать вас более сильным разработчиком. Поглядите на такие техники как Code Retreat, Mikado Method (про него ещё поговорим ниже), test && commit || revert.

В данном же случае на страже стоит самый банальный таймер. Если уж изменения пошли в неправильном направлении, и надо их откатывать, то с потерей 15–20 минут обычно смириться всё же проще, чем с потерей нескольких часов или тем более дней.

Чтобы регулярно укладываться в установленный лимит времени, на кодовой базе надо поддерживать гигиену. Стабильно зелёная сборка, быстрые тесты, быстрый статический анализ — всё пригодится. Если чего‑то из этого нет, постройте первым делом хотя бы какой‑то минимальный базис (не обязательно через мелкофиксы).

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

Принцип 3. Опора на инструменты

Суть: максимально опираться на автоматизированные инструменты в чистках.

Это уменьшает риски вылететь за 15-минутные рамки на 1 чистку. Лимит откровенно тесный, и тратить его на тупое редактирование кода руками будет совершенно неразумно.

Инструменты могут быть разными. Работая в IDE, используем автофиксы и встроенные рефакторинги. Запоминаем хоткеи, на которых они висят. Один хоткей позволяет провернуть типичный рефакторинг типа Extract Method за пару секунд, а также предупредит о наличии блокеров, мешающих это сделать. Если делать такое же изменение руками, это может занять минуты, а фиксы незамеченной проблемы — и того больше.

Осваиваем продвинутые редакторы. Мой выбор — это Vim (как сам по себе, так и в виде плагинов для IDE). Он даёт мне быстрое и эффективное редактирование текста, возможность повтора последного изменения («точка», крайне актуально для точечных изменений), эффективные поиск и замену по регуляркам, макросы для «одноразовой автоматизации» цепочек действий.

Но даже возможностей IDE или редактора не всегда достаточно. Допустим, я заметил, что в унаследованном коде часто используется не подходящее имя переменной. В разных местах встречается упоминание переменных Foo bar = new Foo(..), а вместо этого хотелось бы иметь Foo foo = new Foo(..). Эта потребность может быть продиктована желанием избежать путаницы в коде, потому что класс Bar тоже существует где‑то по соседству (мы отделили его от Foo пару спринтов назад), но отвечает за что‑то совершенно другое. Я могу быстро переименовать в IDE одну переменную в методе, но что, если таких методов в коде 20 или 30? Наши «автоматические переименования» снова превращаются в рутинную ручную операцию, которую надо применять десятки раз. Да и не для всех языков есть мощная поддержка со стороны IDE, верно?

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

sed (stream editor) преобразовывает текст в соответствии с указанными регулярными выражениями. Он хорош прежде всего тем, что идёт в комплекте с практически любой Unix‑совместимой системой. Часто бывает полезен при анализе кода (в связке с find и grep). Есть у него и режим редактирования файлов, но я использую его с крайней осторожностью. Если же риски приемлемы, описанную выше замену можно было бы провернуть следующим однострочником (комбинации \< и \> означают начало и конец слова, соответственно):

# Найти все java-файлы и в каждом заменить слово 'bar' на слово 'foo'
find src -type f -name \*.java | xargs sed -i 's/\<bar\>/foo/g'

Весь код такого рода:

Foo bar = new Foo("Test");
bar.barbarize();

Преобразуется в такой:

Foo foo = new Foo("Test");
foo.barbarize();

Это реально быстро при должном навыке (несколько секунд на печать «заклинания»), но порой вносит не совсем те изменения, что нужно. Поэтому на практике подобное редактирование чаще бывает удобнее осуществлять с помощью инструментов типа codemod или fastmod. В нём мы также указываем регулярные выражения, по которым надо найти заменяемый код, и выражение для подстановки замены. Точно также, мы запускаем однострочник в шелле (codemod написан на Python, поэтому для указания границ слова используется символ \b).

codemod -d src --extension java '\bbar\b' 'foo'

Результат будет таким же, как выше. Но, по‑умолчанию, codemod использует интерактивный режим, в котором мы можем сразу видеть результат замены. Это помогает быстрее понять, есть ли ошибки в регулярке, и при необходимости тут же их отменить, поправить и перезапустить. Так можно на ходу отладить сложное выражение. Также порой случается, что заменять текст надо не сплошняком, а только в определённых местах. Тогда можно вручную пропустить правки в тех местах, где в них нет необходимости.

Интерактивное редактирование в codemod
Интерактивное редактирование в codemod

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

comby — ещё более мощный инструмент, для самых изощрённых ситуаций. Он знает не только регулярки, но ещё и учитывает структуру программ на типичных языках программирования. Эта структура чаще всего строится с помощью сбалансированных пар всевозможных скобок ((), [], {}, <>). Обычные регулярные выражения плохо подходят в случаях, когда мы не знаем заранее, сколько вложенных скобок нам может встретиться. comby же, наоборот, специально заточен под такие случаи.

Например, мы хотим поменять порядок вызова аргументов в функции: превратить a(x, y) в a(y, x). Если какой‑то из входных аргументов является вызовом другой функции, то обычная регулярка может поймать не ту скобку или не ту запятую, и результат получится некорректным.

$ echo 'a(x, y)' | sed 's/(\(.*\), \(.*\))/(\2, \1)/'
a(y, x)                                                         # правильно

$ echo 'a(x, f(y, z))' | sed 's/(\(.*\), \(.*\))/(\2, \1)/'
a(z), x, f(y)                                                   # неправильно!

А comby считает открывающие и закрывающие скобки и поэтому не допустит такой ошибки, заменит аргументы правильно. У него ещё и «заклинания» пишутся более читаемо, чем регулярки!

$ echo 'a(x, y)' | comby -stdin -stdout 'a(:[x], :[y])' 'a(:[y], :[x])' .py
a(y, x)                                                         # правильно


$ echo 'a(x, f(y, z))' | comby -stdin -stdout 'a(:[x], :[y])' 'a(:[y], :[x])' .py
a(f(y, z), x)                                                   # тоже правильно

Каждый из этих инструментов требует времени на освоение, но это оправдывает себя. Не стоит пытаться провернуть на них сложный рефакторинг — но мы к этому и не стремимся! А вот если надо применить одно маленькое и единообразное(!) изменение по всей кодовой базе, то такие инструменты позволяют исправить десятки (если не сотни) файлов буквально за пару минут. Оставшееся время из нашего 15-минутого таймфрейма можно будет с пользой употребить на проверку результата и подробное оформление коммита.

И это очень важно, потому что чистки должны быть максимально безопасными.

Принцип 4. Безопасность

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

 Наша Библия
 Наша Библия

Эта идея неоднократно подчёркивается в классической работе Мартина Фаулера. Однако, боюсь, в наши дни всё больше разработчиков узнаёт про концепцию рефакторинга не из книги Фаулера, а из случайных статей, видео, разговоров на кофе‑пойнтах, чужих коммитов. Во всех этих случаях вопросу корректности изменений может уделяться недостаточное внимание. Что‑то где‑то поменял — вот тебе и «рефакторинг», братишка! Поэтому я считаю важным дополнительно выделить этот принцип.

Если не следить за безопасностью изменений на постоянной основе, то очень скоро Вы начнёте слышать «опять эти ваши рефакторинги всё сломали! нечего их проводить!». Неаккуратные, поспешные, размашистые правки кода, ошибочно выдаваемые за рефакторинг, становятся причиной всеобщего негодования. Тем самым дискредетируя само это слово, особенно в глазах неспециалистов. В самых запущенных случаях дело может дойти и до полного запрета на рефакторинг и чистки (паллиативное лекарство, которое со временем становится хуже исходной «болезни»).

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

Частый кейс, когда принцип безопасности нарушается, — это попытка «срезать углы» в рефакторинге. Когда мы в уме прикидываем цепочку изменений A → B → C → D, может казаться соблазнительным сразу перепрыгнуть несколько шагов, с A на D. На первый взгляд, это может выглядеть как «оптимизация времени». Вместо трёх последовательных правок мы делаем одну. Разве ж это плохо? Но большой прыжок A → D практически наверняка приведёт к необходимости удалить старый код и написать вместо него новый, с ноля. При этом легко потерять из виду какие‑то важные детали и тем самым сломать поведение. Чем больше «оптимизация», тем выше риск.

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

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

Если очень хочется что‑то поменять в коде, но есть опасение, что это может что‑то сломать, не следует проводить это изменение через непрерывный рефакторинг. Создайте полновесный, тяжёлый тикет в таск‑трекере (об этом ниже), убедите команду в необходимости работы, выделите на него дополнительное время, и спокойно всё сделайте. Либо подумайте, можно ли провести одно рискованное изменение через цепочку более безопасных. Иначе вместо «экономии времени» на промежуточных шагах получите гораздо большие затраты на последующем поиске и исправлении дефектов. Как именно проводить декомпозицию, я расскажу дальше.

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

Принцип 5. Частота и регулярность

Суть: «непрерывный рефакторинг» должен проводиться регулярно и постоянно. Иначе что же это за «непрерывность»?

Понятие «часто» само по себе довольно расплывчатое и сильно зависит от контекста. Я считаю, что частота чисток должна быть не ниже, чем частота изменений по «обычным задачам». Но в условиях, когда изменения должны проходить через ревью коллег, вряд ли они обрадуются бомбардировке свежими коммитами каждые 10–15 минут. Должен быть разумный лимит.

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

Вам может показаться, что это всё равно слишком малая частота. Чего можно добиться в проекте, если делать всего 1 маленькую чистку в день? Однако на ум приходит старый мотиватор.

Кажется, из "Атомных привычек"
Кажется, из "Атомных привычек"

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

Печально порой видеть ситуацию, когда в коде годами остаётся устаревший хлам. Если если его аккуратно распилить на несколько маленьких кусков и удалять по одному куску в день, как скоро этот хлам исчез бы? Правильно — через несколько дней. Неделя‑две, и про него можно будет забыть. Но нет же! Лучше мы будем долго мириться с хламом, а потом пытаемся выделить время на их глобальную починку. Это, конечно же, будет большая задача, поэтому время найти трудно. Задача будет откладываться и откладываться, а проблемы будут всё так же оставаться в коде, годами.

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

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

Принцип 6. Асинхронность

Суть: отделять рефакторинги от обычных изменений кода. Каждый рефакторинг должен проходить через отдельный коммит и отдельный PR от чистого master.

Раньше я активно добавлял всевозможные чистки в код, когда работал над обычными текущими тикетами. И таким образом попадал в ловушку из вступления: коммиты получались большими, ревью проходили долго и тяжело, и из‑за медленного темпа работы код толком не улучшался.

Накатывая же чистки асинхронно, мы получаем сразу несколько преимуществ:

  • Существенно снижаем нагрузку на ревьювера. Ему не надо разбираться, где заканчивается рефакторинг и начинается изменение поведения кода. Всё, что не похоже на заявленную атомарную чистку, сразу становится легко заметным. Это ускоряет прохождение ревью.

  • Уменьшаем риск конфликтов слияния, когда надо объединять изменения от разных авторов. Если мы видим, что на текущий момент у коллег идёт работа над модулем X, и высок риск получить конфликты слияния с ними, то можем временно придержать все его переделки. И вместо этого заняться модулем Y, который сейчас никто другой не трогает (но в котором тоже найдётся, что поправить).

  • Дополнительно снижаем риски поломки кода. Если наши изменения содержат только «честный» рефакторинг, не изменяющий поведения, безопасность будет выше.

  • Упрощаем работу с нестабильными тестами. Я надеюсь, что «зелёный master» для вас такая же норма, как и для меня. Но автотесты бывают всякие, некоторые из них порой и мигают. Если тест мигает на обычном изменении, это может потребовать долгого расследования, чтобы понять причину фейла. Если же он мигает на green‑green рефакторинге, то это, весьма вероятно, флак (хотя проверить всё равно полезно).

  • Не блокируем свою текущую работу. С утра переключаемся на master, проводим запланированную 15-минутную чистку, получаем порцию гормонов радости от сделанной работы, отправляем PR. Затем спокойно переключаемся обратно на ветку с текущей задачей и работаем над ней уже с большим энтузиазмом. В течение дня ревьювер, так или иначе, доберётся до коммита с чисткой. Вы же в это время его не ждёте, а спокойно работаете над текущими делами.

  • Ускоряем процесс распространения хорошего кода. Отсутствие блокировок действует и в обратную сторону: подчистки кода больше не должны ждать завершения работы над текущей задачей. Может, надо дополнительно протестировать изменения, или обсудить с ревьювером особенности алгоритма, или доделать ещё вон тот кусок? Не беда, на подчистку кода в параллельной ветке это никак не повлияет. А это, в свою очередь, способствует регулярности.

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

 Чистки кода, смешанные с другой работой, станут доступны всем только после слияния веток
 Чистки кода, смешанные с другой работой, станут доступны всем только после слияния веток

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

Чистки кода, применяемые асинхронно, попадают в главную ветку максимально быстро
Чистки кода, применяемые асинхронно, попадают в главную ветку максимально быстро

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

Принцип 7. Беклог и декомпозиция

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

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

А как выглядит Ваша форма создания тикета?
А как выглядит Ваша форма создания тикета?

Я встречал слишком мало таск‑трекеров (или рабочих процессов на их основе), которые хорошо дружили бы с по‑настоящему маленькими задачами. К сожалению, чаще встречается такое: для создания задачи в основном трекере надо правильно заполнить 5–10–20 полей, поставить оценочку, поставить метки, договориться о её включении в объём текущих работ, подвигать задачу несколько раз по всем стадиям, записать потраченное время... В запущенных случаях это отнимет больше времени, чем вся работа над кодом.

Идеальный беклог для мелкочисток должен быть гораздо проще: это либо простой линейный список планируемых улучшений, либо дерево (об этом ниже). Как его «натянуть» на текущие рабочие правила, Вам придётся решать самостоятельно. Идеально, если Ваши рабочие правила достаточно разумны и не требуют тотальной джирафикации работы.

Твит от какого-то чувака в интернетах (с внушительным импактом, впрочем) говорит про то же самое.
Твит от какого-то чувака в интернетах (с внушительным импактом, впрочем) говорит про то же самое.

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

Что делать с проблемами, которые не решаются одним простым фиксом? Обычно это происходит, когда задача слишком большая, либо её что‑то блокирует.

Большую задачу следует разбить на части и катить за несколько последовательных подходов. Она, конечно, растянется на несколько дней или даже недель — но зато не будет отнимать много внимания, не будет блокировать основную работу. И при аккуратном применении всех принципов всё равно может быть доведена до конца в разумные сроки. Этот вариант хорошо подходит, когда изменения однотипные, но плохо поддаются автоматизации, или когда даже небольшие по объёму изменения требуют достаточно внимательного ревью.

Есть и другой класс проблем. Бывает, что проблему A было бы несложно решить саму по себе, но при попытке это сделать мы упираемся в проблему B, а при попытке решить проблему B — в следующую проблему C (и далее по алфавиту). Знакомо?

Я рекомендую бороться с блокерами через Mikado Method. Техника несложная, но очень эффективная. Суть её в следующем: когда мы проводим изменение в коде и видим, что препятствие мешающее этому изменению, то делаем так:

  1. Откатываем все наши изменения назад.

  2. Убираем препятствие на чистой версии кода.

  3. Возвращаемся к первоначальной задумке и снова пробуем его реализовать.

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

Если на шаге 2 встречаем новое препятствие, то с ними поступаем точно так же. В конечном итоге так мы находим «корневой» блокер — проблему, которую уже ничего не блокирует. Разбираемся с ним, а дальше проводим все остальные изменения обратно по нарисованному графу. Мой опыт показывает, что «корневой блокер» часто бывает совершенно не очевиден с первого взгляда, и другими методами его бывает найти весьма непросто.

Решаем проблемы, которые ничего не блокирует, и так постепенно добираемся до A
Решаем проблемы, которые ничего не блокирует, и так постепенно добираемся до A

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

SiebenApp
SiebenApp

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

Если mikado кажется слишком сложным, ведите хотя бы простой линейный список планируемых подчисток в отдельном файле. Это позволит держать под контролем желание добавлять их в текущие коммиты. Поделитесь списком с командой, обсудите приоритеты. И дальше спокойно в удобном темпе проводите одно маленькое улучшение за другим.

Старайтесь не раздувать список, чтобы выбор очередной подчистки не превращался в сложный квест.

Симбиоз

Некоторые принципы по отдельности могут выглядеть противоречащими «здравому смыслу». Но если объединить всё вместе, то складывается довольно цельная картина.

Почти как в том меме с безумным мужиком!
Почти как в том меме с безумным мужиком!

Эти принципы работают вместе, помогая друг другу:

  • Точечные правки позволяют проще получить быстрые изменения: мы обычно тратим меньше времени на 1 вещь, чем на несколько. С ними проще гарантировать безопасность. Такие правки проще положить на инструменты, чем комплексные. Стимулирует декомпозицию: если правка не‑точечная, то её надо разбивать.

  • Быстрые изменения облегчают достижение частоты и регулярности. Если мы тратим на рефакторинг 15 минут в день, то не так уж и сложно делать это ежедневно. Также, ограничение на длительность рефакторинга само по себе вынуждает делать точечные правки и стимулирует интерес к инструментам. Стимулирует декомпозицию: если правка не влезает в лимит, её надо разбивать.

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

  • Асинхронный подход помогает сделать точечные правки, т.к. мы отказываемся от смешения разнотипных изменений. Также он поддерживает безопасность изменений, когда все изменения проводятся строго от зелёного master.

  • Инструменты помогают в плане скорости. Они расширяют диапазон рефакторингов, которые можно провернуть в рамках быстрых изменений. Они снижают влияние человеческого фактора (опечатки и т. п.), улучшая безопасность.

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

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

Заключение

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

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

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

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

https://twitter.com/jamesshore/status/1550907754927104000

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

Комментарии (14)


  1. Gromilo
    20.11.2023 09:19

    А как быть, если рефакторинг требуется для решения задачи?

    Сначала сделать ветку с рефакторингом и пока он апрувится от этой же ветки делать фичу?

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


    1. MonkAlex
      20.11.2023 09:19
      +1

      Навскидку вижу два подхода

      1. Порефакторить после фичи. Фича первична, рефакторинг обычно не критичен.

      2. Порефакторить внутри задачи. Ревью будет дольше, как автор в статье указал, но это будет целостное (и необходимое?) изменение по задаче.

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


      1. Gromilo
        20.11.2023 09:19
        +1

        Как человек написатель/читатель ПРов вижу такое противоречие:

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

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

        Возможное решение: пока делаешь основную задачу, кидаешь ПРы с микрофакторингами в отдельных ветках. Эти ветки можно подтягивать в свою ветку как до и так после прохождения ревью.


        1. MonkAlex
          20.11.2023 09:19
          +2

          Это пока они друг друга не цепляют, эти изменения. Когда рефакторинг и фича пересекаются, так уже не сделать. Я собственно так и понял было изначальный вопрос.


        1. lgorSL
          20.11.2023 09:19

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

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


        1. zloddey Автор
          20.11.2023 09:19

          Если PR-ов с микрочистками становится много, они начинают тормозить процесс, и мы попадаем в "ловушку 2". Время работы над задачей всё равно растягивается, да и ревьюверы всё равно ворчат, что у них много работы. И вопрос сцепления изменений по-прежнему остаётся острым. Поэтому я всё же предлагаю идеи по чисткам не прокатывать сразу, а сперва закидывать в беклог, "грязный" же код ещё какое-то время терпеть. Если/когда конвейер асинхронных чисток налаживается, то самые неприятные куски кода быстро будут вычищены.


    1. zloddey Автор
      20.11.2023 09:19

      Если рефакторинг прям действительно необходим для текущей задачи, варианта вижу два:

      1. Да, отдельная ветка с предварительным рефакторингом и отдельный PR. Пока аппрувится, работать над самим изменением. Абсолютно нормальный и хороший вариант, хотя порой и приходится несколько больше обычного возиться с git локально.

      2. А порой и всё вместе катить тоже норм, если изменения в целом достаточно небольшие.

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

      Пример: представим, что команда поменяла недавно в проекте базовый уровень языка. Стали доступны новые плюшки синтаксиса, в стандартной библиотеке появились новые функции, которые раньше были только в сторонних зависимостях или собственных утилитах. А какие-то конструкции кода теперь признаны устаревшими.

      Стоило бы обновить код на проекте в целом? Скорее всего, да - это улучшило бы читаемость и понятность, позволило бы упростить код, позволило бы (к примеру) убрать какие-то зависимости, ставшие необязательными. Стоит ли делать это в рамках обычной работы? Скорее всего, нет. Это будет раздувать PR-ы и несколько увеличивать риск поломки. Да и не везде получится поменять стиль - только в "горячих" участках кода. А в "холодных" будет всё по-старому, и стиль на проекте в среднем станет хуже (мешанина старого и нового, выше энтропия).

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

      Чтобы не тормозить текущую задачу, используем любой вид беклога. Записываем идею по чистке вообще в любое место, откуда мы уверены, что её потом прочитаем и не потеряем. Что это за беклог - каждый решает для себя сам. Кому что удобнее: бумажка, файл на компе, голосовуха самому себе в телеге, и т.п. Потом, когда голова не загружена текущей задачей, проверяем свой временный беклог и переносим идею из временного места в официальный беклог для таких чисток. Получается примерно как в GTD или "Джедайских техниках" Дорофеева.


  1. VladimirFarshatov
    20.11.2023 09:19
    +3

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

    В целом неплохая статья со сборкой достаточно актуальных вопросов. Спасибо.


    1. Gromilo
      20.11.2023 09:19
      +3

      На это нужна политическая воля


    1. zloddey Автор
      20.11.2023 09:19

      Хорошо, конечно, когда есть гарантированные 10% на бэклог и рефакторинг. Но такая радость встречается не всегда. Предложенный метод позволяет, среди прочего, работать над качеством кода "ниже радара", чтобы, с одной стороны, не терять фокусировки на текущие задачи (даже если в них никаких чисток не запланировано), а с другой стороны, постепенно снижать боль, которая возникает от "грязного" кода. Я не призываю всегда чистить код "ниже радара", но порой других вариантов не остаётся. Потом, уже при наличии успешной истории чисток, проведённых без ущерба для работы, можно и поднять вопрос о легализации подхода, например.


    1. zloddey Автор
      20.11.2023 09:19

      Вплоть до того, что при раздаче спринта к каждой таске приклеиваются задачи из бэклога

      А этот подход для меня, скорее, антипаттерн (ловушка 2 из вступления). По крайней мере, в том случае, когда изменения по разным задачам смешиваются в одном PR. Если они идут разными патчами/реквестами, то вполне норм.


  1. Tibor128
    20.11.2023 09:19
    +1

    Спасибо за отличную статью! Теперь к привычной цепочке добавилась ещё одна continuous. Однако для меня остаётся открытым вопрос, что делать с комитами? При таком подходе история дико раздуется и всю эту красоту в мастер сливать, мягко говоря - не правильно.


    1. MonkAlex
      20.11.2023 09:19

      В чём проблема?

      Я лично ничего не делаю с историей, получилась фича из 20 коммитов - пусть так и будет.


    1. zloddey Автор
      20.11.2023 09:19

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

      Если же мы говорим про регулярные подчистки, тут я всё же за подход "1 чистка = 1 небольшой коммит = 1 мердж в мастер"). Посмотрите ещё раз "Принцип 6. Асинхронность". Я стараюсь как можно быстрее вливать чистки в master, чтобы они распространились как можно быстрее по всему коду, включая код коллег.