Практики управления техническим долгом в отдельно взятой команде
Примерно год назад наша команда перешла из фазы ускоренного наращивания функциональности к более плавной разработке с упором на повышение качества. К этому моменту в наших продуктах накопилось заметное количество неоптимальных решений, некрасивого кода, устаревших библиотек. Со всем этим надо было что-то делать.
К сегодняшнему дню удалось выстроить процесс, который делает борьбу с техническим долгом предсказуемой, безболезненной и неизбежной.
Что удалось получить в результате:
- Команда довольна. В релизной ретроспективе регулярно фигурируют положительные пункты про совершенствование технологий и уменьшение технического долга.
- Несколько квартальных релизов подряд мы смогли наращивать функциональность без увеличения количества строк кода в проекте. Удаление ненужного кода и упрощение нужного уменьшали размер кодовой базы для существующей функциональности. И это уменьшение как раз примерно совпадало по масштабу с новым кодом, реализующим новую функциональность.
- Во время проведения рефакторингов и модернизаций продукт всегда в рабочем состоянии. Каждые две недели мы выпускаем полностью работающий промежуточный релиз.
Давайте расскажу, как мы этого добились.
Что такое технический долг
Мое рабочее определение технического долга — это количество работы, которую необходимо выполнить, чтобы проект соответствовал представлениям команды о прекрасном. Заметьте, что технический долг может возникать не только за счёт либерального применения костылей в разработке, но и за счёт изменения представлений о прекрасном. Например, изменились общепринятые практики в индустрии. Или разработчики разлюбили ООП и полюбили функциональное программирование. Или когда-то модный фреймворк уже не торт и стало сложно найти специалистов, которые хотели бы на нём писать.
Впрочем, основная причина технического долга — энтропия во всем своем разнообразии. Отключенные юнит-тесты, устаревшие комментарии, потерявшие связь с кодом, неудачные архитектурные решения, реализация фич, которыми больше никто не пользуется, задел на будущее, которое не наступило и многое-многое другое.
Из этого следует, что появление технического долга неизбежно в любом долгоживущем проекте.
Когда технический долг не проблема
Чем плох технический долг? Он увеличивает стоимость дальнейшей разработки за счёт ряда факторов:
- Снижается скорость реализации новой функциональности.
- Увеличивается вероятность регрессий при исправлении дефектов.
- Процесс разработки становится менее предсказуемым, следовательно, менее управляемым.
- Удлиняется процесс ввода нового разработчика в проект.
Эти потери иногда называют «процентами по техническому долгу»
Бывают ситуации, когда эти потери обходятся дешевле, чем устранение технического долга:
- Конец жизни проекта близок. Заметьте, не конец добавления функциональности, а момент, когда можно прекратить тратить усилия программистов и на поддержку тоже. В эту же категорию входят одноразовые прототипы, одноразовые демо-версии для выставок и т.п.
- Ценность времени разработки в текущий момент намного выше, чем ожидается в будущем. Срочные исправления к фиксированному дедлайну, стартапы, у которых заканчиваются деньги с очередного раунда финансирования и т.п. В таких случаях исправление технического долга можно отложить до момента, когда накал страстей снизится. Бывают проекты, которые не вылезают из состояния аврала, но им советы из этой статьи всё равно не помогут.
Что делать с техническим долгом? Неудачные подходы
Подход № 1. «У нас нет времени, релиз надо сдать вчера»
Стремление успеть в завтрашний дедлайн любой ценой, в ущерб скорости разработки послезавтра.
Иногда быстровозводимая конструкция из костылей — объективно правильный выбор. В моей практике это было ярче всего выражено при изготовлении демоверсий к выставкам. Дата мероприятия жестко зафиксирована, если не успел к важной выставке — следующая попытка будет через год. Показывать продукт при этом можно «из рук», аккуратно обходя все баги. Мне, как инженеру, делать такие проекты неприятно, но костыли в них оправданны.
Когда делаешь продукт, который будет жить долго, всё по-другому. Успеть в срок за счёт сомнительных технических решений — дорогое удовольствие. Полная стоимость складывается:
- из реализации самого костыля,
- из его последующей замены на полноценное решение,
- из страданий от наличия костыля в промежутке между предыдущими пунктами.
Второй пункт очень легко недооценить, а про третий, самый дорогой, есть риск не подумать совсем.
Когда встречаешь на конференции техлида с дергающимся глазом, может оказаться, что именно кошмар бесконечной отладки конструкции из костылей привел его в такое состояние.
Подход № 2. «Да тут надо всё выкинуть и написать заново»
Чем хуже ситуация с техническим долгом, тем сильнее искушение похоронить весь код проекта и написать всё заново. Это одна из классических ошибок, способных убить весь проект.
Тема настолько хорошо раскрыта в известной статье Джоэля Спольски, что не вижу смысла приводить своих аргументов.
Things You Should Never Do, Part I
Подход № 3. «Будем рефакторить по ночам и в выходные, чтобы менеджер не узнал»
Аргументировать необходимость устранения технического долга с проекцией на выгоду бизнесу не всегда просто. У команды разработки может возникнуть соблазн обойти острые углы и начать крупный рефакторинг явочным порядком. Сделать это можно в нерабочее время, в паузах между другими задачами или «на хвосте» у других задач за счёт раздувания оценок.
Что в этом плохого? О, целая куча вещей:
- Снижение прозрачности подрывает доверие между менеджментом и командой. Часто следующие за этим попытки исправить ситуацию наведением дисциплины и «закручиванием гаек» приведут к дальнейшему ухудшению командной работы.
- Закрепившаяся ситуация, в которой приоритеты команды и менеджмента противоречат, вызывает демотивацию с обеих сторон.
- Плоды рефакторинга непредсказуемым образом включаются в продукт и вызывают регрессионные баги там, где их не ожидают. В сознательных командах это приводит к внезапной и не планируемой нагрузке на QA. В несознательных уходит в продуктив и ломается уже там.
После применения командой этого рецепта глаз начинает дергаться уже у менеджмента.
Наши принципы
1. Добавлять технические задачи в общий бэклог
Существует ряд закономерностей из жизни задач в проектах:
- Задача, которой нет в бэклоге, имеет меньше шансов попасть в работу.
- Задача без понятной формулировки имеет меньше шансов попасть в работу.
- Задача, которую нельзя с высокой степенью уверенности оценить, имеет меньше шансов попасть в работу.
- Большая задача будет ждать в очереди дольше, чем маленькая.
Про эти вещи очень хорошо рассказывает Максим Дорофеев в своей «Технике пустого инбокса»
Чтобы технический долг не копился, работы по его устранению стоит оформлять с оглядкой на принципы, перечисленные выше.
Все задачи, кроме самых мелких, заводятся в бэклоге. Так у них появляется шанс быть сделанными не только в свободное время, но и в рамках запланированных работ. Кроме того, такие задачи сложнее совсем потерять из виду — на бэклог смотрят чаще и пристальнее, чем на TODO в коде, бумажки на мониторе, заброшенные вики-страницы, залитые чаем салфетки со схемами и прочие источники информации.
- Если в коде есть нетривиальное TODO, оно содержит ссылку на задачу в бэклоге. Мы проверяем соблюдение этого принципа на code review и не принимаем сложные TODO без ссылок.
За комментарием может скрываться драмаОднажды рядом с таким TODO было написано: «Костыль. Product Owner заставил меня сделать это. Уберите как можно скорее».
- Когда разработчик понимает, что какое-то место требует рефакторинга, он создает задачу в бэклоге.
- Когда у разработчика появляется пожелание по улучшению платформы, он создает задачу в бэклоге.
- Долгосрочные архитектурные планы ложатся в бэклог в виде отдельных задач, как только появляется достаточно определенности хотя бы по первым шагам.
Пока такие изменения остаются недорогими с точки зрения затрат разработки и объемов необходимого тестирования, мы можем постепенно улучшать свою кодовую базу, не отрываясь от выполнения бизнес-задач и не внося дополнительных рисков.
2. Планировать технические истории исходя из бизнес-приоритетов
Если в лесу упало дерево, но этого никто не слышал, был ли звук? Если в проекте есть плохой код, но этот модуль никогда не потребуется менять, есть ли технический долг?
Я считаю, что когда программисту неприятно смотреть на какой-то старый модуль — это само по себе не очень большая проблема. Куда хуже то, что происходит, когда в этом модуле потребуется добавлять новую функциональность или расширять старую. По сравнению с внесением изменений в хорошо написанный код, такие задачи чаще (и сильнее) выходят за исходную оценку и содержат больше багов. Иногда намного больше. Чтобы защитить себя от проблем подобного рода, мы стараемся планировать рефакторинги так, чтобы они были сделаны перед написанием новой функциональности в том же месте.
Если изменения функциональности и рефакторинг кажутся небольшими — их можно делать вместе. Эмпирически подобранный размер задачи, для которой такой подход будет оптимальным — 3 дня работы одного разработчика и меньше. Когда видно, что работы больше, она разделяется на рефакторинг с сохранением текущего поведения и реализацию новой функциональности.
Таким образом, порядок работ по устранению технического долга определяется очередностью бизнес-задач в бэклоге.
У принципа «опора на бизнес-приоритеты» есть ещё одно применение. Одна из типичных проблем, от которой страдают разработчики, стремящиеся писать хорошо, — сложности с выделением времени на оптимизацию производительности, улучшение поддерживаемости или другие вещи, которые напрямую не фигурируют в плане работ. Для этих улучшений почти всегда можно найти бизнес-потребность. Кто не хочет, чтобы система работала быстрее, стабильнее, была дешевле в сопровождении? Все эти преимущества можно оценить и исходя из этой оценки — положить задачи на улучшение в бэклог, вместе с любыми другими.
Так что если вам хочется оптимизировать производительность, а приходится вместо этого править очередной скучный баг — возможно, вы просто не умеете объяснить пользу от оптимизации на понятном владельцу продукта языке.
3. Оставлять код чище, чем он был до тебя
Почти любой код, кроме написанного совсем недавно, немного отстает от актуального представления о прекрасном в области стиля и архитектуры. Когда нужно менять код в рамках какой-то задачи, хорошим тоном считается сделать все безопасные улучшения, которые возможны в затронутом участке. Что это может быть?
- Приведение к актуальному стилю оформления модулей.
- Смена внутренних названий переменных на более понятные.
- Упрощение реализации при сохранении поведения.
- Локальные рефакторинги, не вносящие масштабных изменений в другие модули.
От таких улучшений ожидается, что они сделают код лучше, но не будут создавать ощутимого удорожания разработки или тестирования.
За счёт этого принципа качество кода понемногу повышается в фоновом режиме, даже в тех местах, где не планировалось отдельных рефакторингов. При этом чем чаще мы работаем над определенной частью системы, тем лучше код этой части. Приятный контраст с проектами, где больше всего времени разработчик проводит в частях с самым плохим кодом.
4. Что бы ни происходило, система должна оставаться в рабочем состоянии
Один из базовых принципов SCRUM говорит, что в конце каждого спринта система должна приходить в стабильное состояние.
«К концу спринта инкремент должен быть готов, что подразумевает его соответствие критериям готовности скрам‐команды и готовность к использованию. Он должен быть готовым к использованию вне зависимости от решения владельца продукта его выпускать или повременить».
Любые работы по устранению технического долга делаются с соблюдением этого принципа.
Большие преобразования обязательно декомпозируются так, чтобы любой отдельный этап можно было закончить за один спринт. Например, систему сборки мы меняли в два этапа (Angular 1.x: крадущийся webpack, затаившийся grunt)
Мы работаем с VCS по принципам, близким к классическому gitflow. Разработка идёт в фича-ветках, тестирование там же. Как правило, такая ветка живет не дольше одного двухнедельного спринта. Ветка, живущая дольше, почти всегда приводит к дополнительным затратам.
Наш опыт четко подтверждает эту закономерность. Каждый раз, когда нам не удавалось закончить большой рефакторинг за две недели, это сопровождалось болью и страданиями. И чем дольше была задача и дольше жила открытая ветка, тем медленнее шла работа и тем больше было проблем.
Потребность всегда быть в нескольких шагах от стабильного релиза создает одну из самых сложных и интересных инженерных задач — поиск оптимальной декомпозиции стратегических планов. Масштабные изменения можно разложить на отдельные независимые шаги. Желательно так, чтобы начать получать пользу как можно раньше. Чем лучше проведена такая разбивка работ, тем больше шансов довести дело до конца.
Как выглядит наш процесс
Раз в релиз делаем подробное ревью технического бэклога:
- Закрываем неактуальные истории (потерявшие актуальность, сделанные в рамках чего-то ещё, дубликаты).
- Обновляем описание там, где видение вопроса изменилось.
При появлении бизнес-историй на горизонте делается технический анализ и с бизнес-историей связываются все технические истории, которые помогли бы в реализации.
При подготовке к планированию спринта:
- Проверяем связи технических и бизнес-историй.
- Привязываем к техническим историям все связанные баги, которые можно дешево исправить в том же месте.
Как формировать техническую часть бэклога
Придя на роль лида в команду, я спросил каждого разработчика и QA, какие улучшения в продукте они больше всего хотят сделать. Большинство пожеланий было связано с техническими улучшениями платформы и рефакторингами. Как показал дальнейший опыт, все ключевые технические проблемы продукта вошли в этот набор пожеланий. Так что этой практикой можно пользоваться, чтобы быстро сформировать технический бэклог с нуля или получить общее представление о состоянии с техническим долгом в новом для вас проекте.
Текущее наполнение бэклога техническими задачами происходит за счёт описанных выше практик и не требует отдельных усилий или анализа. Помимо этого, в бэклог добавляются все новые идеи по техническому совершенствованию продукта. Это делает любой член команды, которому такая идея пришла в голову. Главное на этом этапе — не потерять идею. Уточнение и определение приоритета происходят уже потом, в ходе планирования работ.
Выводы
- Технический долг неизбежен.
- Для большинства проектов устранение технического долга — это хорошее вложение усилий.
- Если устранением технического долга не заниматься, скорость разработки будет постепенно стремиться к нулю.
- Искушение выкинуть все и написать заново может убить или сильно покалечить ваш проект.
- Пользу от устранения технического долга стоит формулировать в ключе пользы для бизнеса, иначе есть риск того, что задачи по созданию новой функциональности всегда будут считаться более важными.
- Задачами по устранению технического долга стоит управлять. Такие задачи ничем не отличаются от других проектных задач в плане учета, планирования, приоритизации.
- Регулярно возникают ситуации, когда можно уменьшить технический долг и одновременно решить бизнес-задачу дешевле, чем сделать это по отдельности. Эти возможности надо использовать.
- Продуманные и вовремя обновляемые соглашения о стиле кода и процесс ревью помогает замедлить возникновение нового технического долга.
- Короткие итерации полезны для рефакторингов не меньше, чем для разработки новой функциональности.
- Команда обычно знает, где в проекте технический долг и насколько он страшен. Стоит использовать это знание при формировании представления о техническом долге проекта.
Автор: alexanderlebedev