Практики управления техническим долгом в отдельно взятой команде


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


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


Что удалось получить в результате:


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

Давайте расскажу, как мы этого добились.



Что такое технический долг


Мое рабочее определение технического долга — это количество работы, которую необходимо выполнить, чтобы проект соответствовал представлениям команды о прекрасном.  Заметьте, что технический долг может возникать не только за счёт либерального применения костылей в разработке, но и за счёт изменения представлений о прекрасном. Например, изменились общепринятые практики в индустрии. Или разработчики разлюбили ООП и полюбили функциональное программирование. Или когда-то модный фреймворк уже не торт и стало сложно найти специалистов, которые хотели бы на нём писать.


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


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


Когда технический долг не проблема


Чем плох технический долг? Он увеличивает стоимость дальнейшей разработки за счёт ряда факторов:


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

Эти потери иногда называют «процентами по техническому долгу»


Бывают ситуации, когда эти потери обходятся дешевле, чем устранение технического долга:


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

Что делать с техническим долгом? Неудачные подходы


Подход № 1. «У нас нет времени, релиз надо сдать вчера»


Стремление успеть в завтрашний дедлайн любой ценой, в ущерб скорости разработки послезавтра.


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


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


  1. из реализации самого костыля,
  2. из его последующей замены на полноценное решение,
  3. из страданий от наличия костыля в промежутке между предыдущими пунктами.

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


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



Подход № 2. «Да тут надо всё выкинуть и написать заново»


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


Тема настолько хорошо раскрыта в известной статье Джоэля Спольски, что не вижу смысла приводить своих аргументов.


Things You Should Never Do, Part I


Подход № 3. «Будем рефакторить по ночам и в выходные, чтобы менеджер не узнал»


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


Что в этом плохого? О, целая куча вещей:


  • Снижение прозрачности подрывает доверие между менеджментом и командой. Часто следующие за этим попытки исправить ситуацию наведением дисциплины и «закручиванием гаек» приведут к дальнейшему ухудшению командной работы.
  • Закрепившаяся ситуация, в которой приоритеты команды и менеджмента противоречат, вызывает демотивацию с обеих сторон.
  • Плоды рефакторинга непредсказуемым образом включаются в продукт и вызывают регрессионные баги там, где их не ожидают. В сознательных командах это приводит к внезапной и не планируемой нагрузке на QA. В несознательных уходит в продуктив и ломается уже там.

После применения командой этого рецепта глаз начинает дергаться уже у менеджмента.


Наши принципы


1. Добавлять технические задачи в общий бэклог


Существует ряд закономерностей из жизни задач в проектах:


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

Про эти вещи очень хорошо рассказывает Максим Дорофеев в своей «Технике пустого инбокса»


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


Все задачи, кроме самых мелких, заводятся в бэклоге. Так у них появляется шанс быть сделанными не только в свободное время, но и в рамках запланированных работ. Кроме того, такие задачи сложнее совсем потерять из виду — на бэклог смотрят чаще и пристальнее, чем на TODO в коде, бумажки на мониторе, заброшенные вики-страницы, залитые чаем салфетки со схемами и прочие источники информации.


  • Если в коде есть нетривиальное TODO, оно содержит ссылку на задачу в бэклоге. Мы проверяем соблюдение этого принципа на code review и не принимаем сложные TODO без ссылок.
    За комментарием может скрываться драма
    Однажды рядом с таким TODO было написано: «Костыль. Product Owner заставил меня сделать это. Уберите как можно скорее».
  • Когда разработчик понимает, что какое-то место требует рефакторинга, он создает задачу в бэклоге.
  • Когда у разработчика появляется пожелание по улучшению платформы, он создает задачу в бэклоге.
  • Долгосрочные архитектурные планы ложатся в бэклог в виде отдельных задач, как только появляется достаточно определенности хотя бы по первым шагам.

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


2. Планировать технические истории исходя из бизнес-приоритетов


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


Я считаю, что когда программисту неприятно смотреть на какой-то старый модуль — это само по себе не очень большая проблема. Куда хуже то, что происходит, когда в этом модуле потребуется добавлять новую функциональность или расширять старую. По сравнению с внесением изменений в хорошо написанный код, такие задачи чаще (и сильнее) выходят за исходную оценку и содержат больше багов. Иногда намного больше. Чтобы защитить себя от проблем подобного рода, мы стараемся планировать рефакторинги так, чтобы они были сделаны перед написанием новой функциональности в том же месте.


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


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


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


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


3. Оставлять код чище, чем он был до тебя


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


  • Приведение к актуальному стилю оформления модулей.
  • Смена внутренних названий переменных на более понятные.
  • Упрощение реализации при сохранении поведения.
  • Локальные рефакторинги, не вносящие масштабных изменений в другие модули.

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


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


4. Что бы ни происходило, система должна оставаться в рабочем состоянии


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


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

–SCRUM Guide


Любые работы по устранению технического долга делаются с соблюдением этого принципа.


Большие преобразования обязательно декомпозируются так, чтобы любой отдельный этап можно было закончить за один спринт. Например, систему сборки мы меняли в два этапа (Angular 1.x: крадущийся webpack, затаившийся grunt)


Мы работаем с VCS по принципам, близким к классическому gitflow. Разработка идёт в фича-ветках, тестирование там же. Как правило, такая ветка живет не дольше одного двухнедельного спринта. Ветка, живущая дольше, почти всегда приводит к дополнительным затратам.


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


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


Как выглядит наш процесс


Раз в релиз делаем подробное ревью технического бэклога:


  • Закрываем неактуальные истории (потерявшие актуальность, сделанные в рамках чего-то ещё, дубликаты).
  • Обновляем описание там, где видение вопроса изменилось.

При появлении бизнес-историй на горизонте делается технический анализ и с бизнес-историей связываются все технические истории, которые помогли бы в реализации.


При подготовке к планированию спринта:


  • Проверяем связи технических и бизнес-историй.
  • Привязываем к техническим историям все связанные баги, которые можно дешево исправить в том же месте.

Как формировать техническую часть бэклога


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


Текущее наполнение бэклога техническими задачами происходит за счёт описанных выше практик и не требует отдельных усилий или анализа. Помимо этого, в бэклог добавляются все новые идеи по техническому совершенствованию продукта. Это делает любой член команды, которому такая идея пришла в голову. Главное на этом этапе — не потерять идею. Уточнение и определение приоритета происходят уже потом, в ходе планирования работ.


Выводы


  • Технический долг неизбежен.
  • Для большинства проектов устранение технического долга — это хорошее вложение усилий.
  • Если устранением технического долга не заниматься, скорость разработки будет постепенно стремиться к нулю.
  • Искушение выкинуть все и написать заново может убить или сильно покалечить ваш проект.
  • Пользу от устранения технического долга стоит формулировать в ключе пользы для бизнеса, иначе есть риск того, что задачи по созданию новой функциональности всегда будут считаться более важными.
  • Задачами по устранению технического долга стоит управлять. Такие задачи ничем не отличаются от других проектных задач в плане учета, планирования, приоритизации.
  • Регулярно возникают ситуации, когда можно уменьшить технический долг и одновременно решить бизнес-задачу дешевле, чем сделать это по отдельности. Эти возможности надо использовать.
  • Продуманные и вовремя обновляемые соглашения о стиле кода и процесс ревью помогает замедлить возникновение нового технического долга.
  • Короткие итерации полезны для рефакторингов не меньше, чем для разработки новой функциональности.
  • Команда обычно знает, где в проекте технический долг и насколько он страшен. Стоит использовать это знание при формировании представления о техническом долге проекта.

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


  1. dzavalishin
    07.09.2017 13:05
    +1

    Спасибо. Ценно то, что описание получилось компактным и структурным. Его можно брать за основу при формировании регламента.


    1. alexanderlebedev Автор
      07.09.2017 16:02

      Спасибо!


  1. StanislavL
    07.09.2017 14:28
    -2

    Оставлять код чище, чем он был до тебя


    Вот с этим есть сомнения. Например переворматирование прекрасно рушит annotate и потом не найдешь (по крайней мере без больших усилий) кто и зачем написал эту строку. Таск другой и как узнать бизнес логику оригинального таска? А уж смена названий переменных это потенциалльно конфликт у разработчиков (было пару раз на моей памяти).
    Опять же QA могут быть сильно против ибо регресс. При 100% покрытии тестами еще так сяк, но зазеленить упавшее может потребовать долгого времени.


    1. alexanderlebedev Автор
      07.09.2017 15:59
      +1

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

      1. Необходимость смотреть полную историю изменений файла вместо blame/annotate — тут надо выбирать между простой историей изменений и согласованным стилем кода в проекте. Способа получить и то и другое одновременно я не вижу. ИМХО, польза от согласованного стиля более весома, поэтому мы делаем стилистические правки. В других проектах ситуация может отличаться.

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

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


    1. dzavalishin
      07.09.2017 16:21
      +2

      Бизнес-логику оригинального таска надо узнать из документа requirements, ссылку на который разработчик разместил в комментариях к коду, нет?


      1. alexanderlebedev Автор
        07.09.2017 18:53
        +1

        Откуда брать требования к бизнес-логике — интересный вопрос.

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


  1. Sarymian
    08.09.2017 09:24
    +1

    При опытной эксплуатации убедился, что разработка ПО в России «для госнужд» всё так же на стадии «тяп-ляп да побыстрее», главное хайп.
    После ОЭ остался с резко негативным впечатлением… будем надеяться, что хоть когда нибудь ситуация изменится.


  1. kolayuk
    08.09.2017 18:58

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

    КАК?


    1. alexanderlebedev Автор
      08.09.2017 20:44

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

      Качество. Устранение технического долга в важном модуле позволит повысить качество и снизить шанс возникновения регрессионных дефектов при внесении модификаций. Наличие плохо спроектированного/реализованного кода, напротив, гарантирует высокий шанс регрессий. При этом никакой QA-процесс не гарантирует 100% обнаружения дефектов. Значит, появление критичных дефектов, которые могут испортить репутацию компании в глазах широкой публики или ключевого клиента — вопрос времени.

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

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


  1. qw1
    10.09.2017 23:00

    Есть соблазны отрефакторить без заведения тасков.

    1) Я не уверен, что вытяну рефакторинг до конца. Т.е. попробовал сменить алгоритм — одно потянуло другое, входные данные по-другому надо генерить, и вот я уже 4 часа сижу переписываю всё на новый формат данных, и конца этому нет (а думал, что за час успею). Т.е. придётся переписать пусть не всю систему, но подсистему. И тут надо остановиться, откатить все изменения и дать проблеме побродить в голове. Через месяц может будет озарение, что надо заходить с другой стороны.

    2) Есть некоторые идеи, которые не гарантируют улучшение. Например, заменить структуру в памяти с RB-дерева на B-дерево. То есть, думаешь, что выиграешь из-за улучшения локальности данных, а проигрываешь из-за большей O-константы в алгоритмах.

    Т.е. таск надо писать в виде: «я попробую потратить часа 4, а если не пойдёт, то отложить на пару месяцев?»


    1. alexanderlebedev Автор
      11.09.2017 10:28

      Знакомые соображения, перекликаются с моим опытом.

      1. В каждом проекте есть свой пороговый размер задачи, до которого быстрее сделать без планирования. У нас это 2-3 часа. Если есть какая-то идея по улучшению, которая вписывается в это время, можно пробовать без заведения задачи. Главное — вовремя остановиться, если понял, что быстро закончить не получается. Если какой-то эксперимент выглядит дороже этих 2-3 часов, то (в среднем) быстрее добавить его в план и дождаться его очереди, чем ждать, пока откроется окно нужных размеров для экспериментов.

      2. Любые задачи с непредсказуемым итогом мы стараемся записывать в формулировке, очень приближенной к тому, что вы предлагаете. «Попробовать ускорить время старта (лимит времени 1 день)». «Разобраться, почему тесты показали ухудшение производительности в предыдущем спринте и попробовать починить (лимит 4 часа)».

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


      1. alexanderlebedev Автор
        11.09.2017 11:30

        *Upd* Общее количество мини-исследований до 2-3 часов должно быть незначительным и не влиять на темп запланированных работ. Если их становится много, тоже стоит планировать. При нарушении этого принципа снижается прозрачность происходящего для менеджмента и может возникнуть ситуация, идентичная «рефакторингу в тайне от менеджера»