Technical debt что это
Технический долг
Программные системы склонны к накоплению мусора — внутренних недоработок, которые затрудняют дальнейшее изменение и расширение системы по сравнению с идеальным кодом. Технический долг — это метафора, придуманная Уордом Каннингемом. Она объясняет, как воспринимать этот мусор, по аналогии с финансовым кредитом. Дополнительные усилия, необходимые для добавления новых функций, — это проценты по кредиту.
Представьте, что в моей кодовой базе есть модуль путаной структуры. Нужно добавить новую функцию. Если бы структура была ясной, то работа заняла бы четыре дня, но с этим мусором требуется шесть дней. Разница в два дня — это и есть проценты по долгу.
Что меня больше всего привлекает в метафоре долга, так это ясное понимание расходов на его обслуживание или устранение. Я могу потратить пять дней на очистку модульной структуры, устранение мусора, метафорически расплатившись с «кредитором». Если я это сделаю для данной функции, то окажусь в проигрыше, потому что потрачу девять дней вместо шести. Но если появятся ещё две такие функции, то выгоднее всё-таки удалить мусор.
С такой формулировкой задача становится чисто математической. Её решит любой менеджер с электронной табличкой. К сожалению, мы не можем измерить производительность, поэтому никакие затраты объективно не измеримы. Мы можем примерно прикинуть, сколько времени требуется для разработки функции, сколько займёт работа после удаления мусора — и предположить стоимость его удаления. Но точность таких оценок довольно низкая.
Поэтому обычно лучший вариант — поступать так, как мы обычно поступаем с финансовыми долгами: постепенно выплачивать основную сумму. На первой функции я потрачу дополнительные пару дней, чтобы удалить часть мусора. Этого может быть достаточно, чтобы снизить проценты по долгу, так что в будущем его можно будет погасить за один день. Это по-прежнему дополнительное время, но после удаления мусора будущие изменения стали дешевле. Большое преимущество постепенного улучшения заключается в том, что естественным образом мы больше времени тратим на удаление мусора в часто изменяемых областях. Это именно те области кодовой базы, которые наиболее нуждаются в удалении мусора.
Сравнение выплаты процентов с выплатой основного долга помогает определить, с каким мусором разбираться. Если у меня есть какая-то особо ужасная область кодовой базы, то проблема исчезает, если выясняется, что мусор невыгодно удалять. Проценты выплачиваются только тогда, когда приходится работать с этой частью программного обеспечения (в этом месте метафора немного хромает, потому что по банковскому кредиту нужно платить всегда). Таким образом, кошмарные, но стабильные области кода можно оставить в покое.
В отличие от стабильных частей кода, области высокой активности требуют нулевой терпимости к мусору, потому что там процентные платежи чрезвычайно высоки. Это особенно важно, поскольку мусор накапливается там, где разработчики делают изменения, не обращая внимания на качество: чем больше изменений, тем больше риск появления мусора.
Метафора долга иногда используется для оправдания низкого качества кода. Аргумент состоит в том, что качественный код требует больше времени и усилий. Если срочно нужны новые функции, лучше принять на себя долг и разбираться с ним потом
Опасность здесь в том, что зачастую этот анализ ошибочен. Мусор очень быстро начинает вредить, замедляя внедрение новых функций. В итоге разработчики превышают кредитные лимиты и выпускают продукт позже, чем если бы сразу потратили время на обеспечение более высокого качества. Здесь метафора часто вводит людей в заблуждение, поскольку динамика технического долга на самом деле не соответствует динамике финансовых кредитов. Принятие на себя долга для ускорения выпуска продукта работает только в том случае, если вы остаётесь ниже линии design payoff в гипотезе об устойчивости архитектуры, а разработчики пересекают её уже через несколько недель, а не месяцев.
Регулярно ведутся дебаты о том, следует ли считать долгом различные виды мусора. Мне кажется, тут полезно учитывать, берётся долг сознательно и разумно — или безрассудно. Так появился квадрат технического долга.
Дальнейшее чтение
Насколько я могу судить, Уорд впервые представил эту концепцию в отчёте для OOPSLA 1992. Она также обсуждалось в вики.
Есть видеозапись, где Уорд Каннингем обсуждает придуманную им метафору.
Дэйв Николетт расширяет взгляд Уорда на технический долг, приводя прекрасный пример того, что я называю разумным преднамеренным долгом.
Несколько читателей предложили другие годные метафоры. Дэвид Панарити назвал некачественную разработку дефицитом программирования. Судя по всему, он начал использовать этот термин несколько лет назад, когда он соответствовал политике правительства; полагаю, теперь это снова актуально.
Скотт Вуд предложил рассматривать «техническую инфляцию как потерю почвы, когда текущий уровень технологии настолько превосходит уровень вашего продукта, что он постепенно выпадает из окружения. Программы отстают в версиях языка до такой степени, что код больше не совместим с основными компиляторами».
Стив Макконнелл выделил в метафоре несколько хороших моментов. В частности, как уменьшение непреднамеренного долга оставляет больше возможностей намеренно принять долг, когда это полезно. Мне также нравится его формулировка минимальных платежей (которые слишком высоки, чтобы исправить проблемы во встраиваемых системах, но не на сайтах).
Хенрик Книберг утверждает, что наибольшую проблему вызывает старый технический долг, и что разумно установить качественный потолок долга, чтобы им легче было управлять.
Эрик Дитрих обсуждает человеческие потери из-за технического долга: сражения в командах, деградация навыков и выгорание.
Технический долг
Будь вы простым программистом, матерым лидом, архитектором или даже ПМ-ом, вы наверняка в своей нелегкой работе сталкивались с проблемой выбора при добавлении в систему новой возможности. Одно решение гораздо проще реализовать в сжатые сроки и успеть к очередному очень важному релизу, однако оно будет более затратное в сопровождении, менее расширяемое или менее надежное. Другое решение может не обладать всеми этими недостатками, однако обладать другим, в некоторых случаях более важным недостатком – на его реализацию потребуется значительно больше времени.
При этом самым сложным при выборе того или иного решения является «коммуникация» своего выбора непосредственному руководителю, чтобы он смог принять взвешенное решение. А поскольку с точки зрения большинства руководителей «взвешивание» заканчивается сразу же после того, как он услышит сроки реализации, то «коммуникация» заканчиваются примерно через 37 секунд после ее начала (обычно именно столько времени нужно руководителю, чтобы узнать ответ на очень простой вопрос, выражаемый одним словом: «Когда?»)
Не удивительно, что многие простые программисты, матерые лиды и архитекторы, а иногда даже ПМ-ы, которые понимают, что им самим придется расхлебывать проблемы «близоруких» решений, с таким подходом не согласны. И совершенно не удивительно, что с подобной проблемой сталкивались и другие известные и не очень люди, которые придумали типовые «паттерны», описывающие подобную ситуацию. Одним из таких паттернов, является метафора технического долга, впервые описанная Вардом Каннингемом (Ward Cunningham) (*) без малого двадцать лет назад.
Технический долг в классическом понимании
В классическом понимании, т.е. в том виде, в котором эта метафора была описана Вардом Каннингемом, под техническим долгом понимается осознанное компромиссное решение, когда заказчик и ключевые разработчики четко понимают все преимущества от быстрого, пусть и не идеального технического решения, за которое придется расплатиться позднее. И хотя с точки зрения многих разработчиков ситуация, когда плохое решение может быть хорошим, может показаться безумной, на самом деле, это вполне возможно: если краткосрочное решение позволит компании получить видимые преимущества, выпустить продукт раньше конкурентов, удовлетворить ключевого заказчика или каким-то другим образом получить преимущества перед конкурентами, тогда такое решение совершенно оправданно. Иногда это может быть единственным способом, чтобы долгосрочная перспектива вообще существовала.
Можно провести параллель между техническим долгом и долгом финансовым. Финансовый долг означает, что вы получаете дополнительные средства сейчас, однако каждый месяц (или каждые пол года) вам придется выплачивать фиксированную процентную ставку, а в конце срока вернуть весь долг кредитору. Аналогичная ситуация происходит и в случае принятия неоптимального технического решения: вы получили готовую систему или новую возможность уже сегодня, однако при добавлении новой возможности вам придется платить «проценты», либо погасить ваш долг путем рефакторинга системы или части системы.
Именно такой подход к построению приложений является наиболее оптимальным с архитектурной точки зрения. Если принимая архитектурное решение вы не чувствуете компромисса между краткосрочными и долгосрочными выгодами, то в любом случае не стоит вырубать его в камне. Вполне возможно, что вы приняли неправильное решение неосознанно, а из-за своего непонимания предметной области, или из-за будущего изменения требований заказчиком. Значительно проще снизить стоимость будущих изменений путем инкапусуляции важных решений в наименьшее число модулей, нежели стараться продумать архитектуру до мельчайших подробностей, в надежде учесть все возможные и невозможные сценарии. Для определенного круга задач идеальная продуманная архитектура возможна, а иногда просто необходима, но в большинстве случаев – это не так; рано или поздно придет момент, когда ваше видение системы или видинение системы заказчиком изменится, что потребует внесения в нее существенных изменений.
Однако даже при разумном накоплении технического долга существует большая вероятность, что команда (или заказчики) пойдут по старому принципу – работает – не трогай, и никогда не вернутся к этому решению снова, чтобы расплатиться за свой технический долг. Кроме того, существует множество случаев, когда технический долг накапливается постепенно и неосознанно, и если разработчики не будут об этом задумываться, то придут к старой как мир ситуации, когда стоимость добавления новой возможности становится невероятно дорогой, все работают, устают, злятся друг на друга, не получив при этом никаких преимуществ перед конкурентами. Это приводит нас ко второму источнику технического долга – грязному коду (***)
Грязный код, как источник технического долга
Технический долг в классическом понимании является преднамеренным и в основном касается стратегических решений, поэтому и ответственность за него лежит на заказчиках матерых лидах, архитекторах и даже ПМ-ах, но вот все, что связано с грязным кодом касается по большей части простых разработчиков. На самом деле, разница между грязным кодом и неоптимальным стратегическим решением, по сути, не такая уж и большая: при добавлении новой возможности в систему вам приходится расплачиваться за недальновидность в прошлом.
Во время кодирования, как и во время принятия любых других решений, разработчик должен рассматривать краткосрочные и долгосрочные выгоды. Давайте рассмотрим юнит-тесты. Если спросить адепта TDD о необходимости юнит-тестов, то он скажет: «г@#но-вопрос, юнит-тесты должны быть для каждого класса, модуля или функции, и, конечно же, они должны писаться до написания кода». Однако если послушать Кента Бека (****), автора TDD, то его отношение к юнит-тестам более прагматичное. Принимая решение об использовании TDD или серьезном покрытии кода юнит-тестами точно так же нужно принимать во внимание краткосрочные и долгосрочные выгоды. Безусловно, юнит-тесты очень полезны, но они полезны, прежде всего, в долгосрочной перспективе, а что если вы осознаете, что существует высокая вероятность того, что этих долгосрочных перспектив не будет вовсе? Вы можете разрабатывать прототип или что-то типа proof of concepts, и пытаетесь выяснить, будет ли вообще это решение работать или нет. С аналогичной ситуацией неэкономичности юнит тестов можно столкнуться во многих реальных приложениях, когда при добавлении новой возможности стоимость написания юнит-теста может в несколько раз превышать стоимость самой реализации.
Однако это скорее исключение из правил, нежели типовая ситуация. Обычно в процессе развития приложения накапливается как груз крупных стратегических ошибок или непониманий требований, так и масса мелких тактических ошибок, типа длинных функций с неочевидными обязанностями и сложными побочными эффектами; расплывчатых классов, с нечеткими границами и обязанностями; отсутствие идиом именования и документации и т.п… Все это приводит к классическому синдрому разбитых окон, когда ни у кого не возникает идеи поступать по другому, а у кого и возникает, то она быстро пропадает из-за того, что плыть против подобного течения очень и очень сложно.
И если команда не отдает свои долги путем переосмысливания существующих решений и их последующего рефакторинга, если она не старается держать качество кода на высоком уровне, то рано или поздно это повысит стоимость развития и сопровождения кода настолько, что все существующие средства будут уходить на оплату процентов по техническому долгу. Что в свою очередь приведет к низкой продуктивности разработчиков (еще бы, ведь они только и делают, что борются с ветряными мельницами), и взаимной неудовлетворенности команды разработчиков и заказчика.
Выводы
Измерение продуктивности программиста или команды программистов дело сложное, и именно из-за этого возникают многие сложности при выборе того или иного технического решения: руководители или заказчики просто не понимают, какие последствия ждут их при выборе одного и отказе от другого решения. Использование метафоры технического долга не является панацеей и вряд ли обеспечит решающий перевес при выборе того или иного подхода, однако, как минимум, позволит понять ключевым заинтересованным людям проблему в терминах, доступных простому смертному, и покажет проблему пусть и в абстрактных, но все же в финансовых единицах.
Кроме того, даже если вам приходится идти на компромисс и влезать в долговую яму осознанно или не осознанно, старайтесь проектировать ваши системы так, чтобы влияние неоднозначных решений было минимальным, а качество кода реализации – на высоком уровне.
(*) Вард Каннингем – это известный дядька, оказавший невероятный вклад в развитие компьютерного сообщества; он «папа» wiki, а также один из авторов «паттернов» и «экстремального программирования». Информацию по поводу первого вклада можно найти в Википедии, а по поводу второго – в статье «Шаблоны проектирования. История успеха».
(**) Конечно же, речь идет только о тех случаях, когда каждое из приведенных решений кажется более оптимальным, но стоимость его применения именно сейчас кажется неоправданно высокой.
(***) Некоторые авторы, включая Боба Мартина не считают, грязный код (messy code) техническим долгом, однако подобный код увеличивает стоимость добавления новой возможности, так что мне кажется, что его тоже можно рассматривать, как один из видов технического долга.
(****) Здесь речь идет о подкасте Software Engineering Radio Episode 167: The History of JUnit and the Future of Testing with Kent Beck, в котором Кент Бек как раз и высказал эту заветную мысль.
Технический долг на проекте или выбраться из черной дыры
Природа технического долга
Само понятие технического долга впервые ввел Уорд Каннингем (Ward Cunningham), разработчик технологии wiki и один из создателей экстремального программирования. В 1992 г. в своей статье он сформулировал это понятие в виде метафоры финансового долга: так же, как и при финансовом займе, разработчики могут получить дополнительные средства здесь и сейчас за счет использования быстрых и не оптимальных технических решений, но неизбежно будут расплачиваться за них при дальнейшей разработке, в независимости от того, как будет выплачен этот долг — постепенными выплатами процентов или одним платежом.
Но если проблема технического долга была описана еще 25 лет назад и встречается практически на любом проекте, почему еще нет методики управления проектами, которая позволяла бы избежать само появление технического долга? Ответ кроется в самом понятии проекта. Одним из ключевых отличий проекта от других видов деятельности является уникальность конечного продукта. Там где уникальность, там и непредсказуемость, и именно она порождает изменения на проекте и вносит трудности в первоначальное проектирование системы.
Конечно, можно попытаться построить архитектуру, предусматривая возможные изменения, но здесь команда столкнется с таким понятием как “кошелёк Миллера”: правилом, при котором в кратковременную память человека можно «положить» одновременно не более девяти «монет». А если количество элементов превышает это значение, то мозг пытается сгруппировать информацию так, чтобы их количество было от пяти до девяти.
Можно попытаться делить компоненты на более мелкие, чтобы уложиться в этот “кошелек”, но сложности от этого меньше не станет, да и количество абстракций при таком подходе будет расти с катастрофической скоростью. А как известно, любую проблему можно решить путём введения дополнительного уровня абстракции, кроме проблемы слишком большого количества уровней абстракции.
Другие команды предпочитают вообще отказываться от первоначального проектирования, максимально стараясь использовать по ходу разработки универсальные инструменты. С одной стороны, это проще, чем пытаться предсказывать изменения и на первых этапах система будет получаться достаточно гибкой к изменениям. Но со временем, сложность системы, согласно второму правилу Лемана, неизбежно будет расти, она станет менее гибкой, и могут возникнуть изменения, идущие вразрез с текущей архитектурой. В этом случае разработчики будут также тратить больше времени на решение архитектурных проблем.
Так или иначе, неизбежность изменений на проекте провоцирует появление технического долга.
Как определить, что на проекте есть проблема технического долга
Важнейшим показателем того, что на проекте есть проблема с техническим долгом, это, конечно же, сам код. Прежде всего, стоит сказать об особенности написания кода. Дело в том, что написание и чтение кода — два совершенно разных процесса. Когда разработчик пишет код, он сосредоточен лишь на контексте задачи. Но изменение отдельного участка кода влияет на общую картину происходящего. В свою очередь, прежде чем изменять написанный код, нужно иметь представление не только о конкретном участке, но и обо всей картине в целом. Но при чтении, границы контекстов, в рамках которых был написан код, стираются на общем представлении. Такая разница между чтением и написанием кода генерирует дополнительную сложность, которая прямым образом влияет на его качество.
На что следует обратить внимание:
Методы устранения технического долга
Заключение
Проблема технического долга весьма актуальна на данный момент, и для каждого проекта требуется индивидуальный подход к проблеме его устранения.
Не стоит бояться выплат, ведь если принять правильную стратегию, то технический долг станет той силой, которая будет заставлять развиваться ваш проект. Именно работа с техническим долгом показывает уровень зрелости как самого проекта, так и команды, которая над ним трудится.
Стратегии выплаты технического долга
Технический долг: он есть у всех, и каждый достойный своего звания разработчик хочет его выплатить, но как же организовать этот процесс?
Реализуем севооборот
В своей предыдущей статье я сравнил выплату технического долга с важностью севооборота в сельском хозяйстве. Если вы продолжаете обрабатывать поле (кодовую базу) сезон за сезоном, чтобы получать большой урожай (завершать проекты, добавлять фичи и т.п.), и не даёте этому полю сезон на восстановление (выплату технического долга), то оно постепенно начинает терять своё качество и урожайность.
Эта метафора остаётся подходящей и для разработки ПО; кроме того, она содержит в себе намёки на возможные стратегии, которые можно использовать для выплаты технического долга.
Существует на удивление широкий диапазон способов выплаты долга. И это очень полезно, поскольку предоставляет нам множество вариантов при планировании.
В рамках этой статьи мы будем предполагать, что вы работаете в методологии agile-разработки, однако многие принципы при условии творческой переработки применимы и к другим методологиям.
Специальные спринты для выплаты технического долга
В полной аналогии с севооборотом мы можем прекращать работу над фичами на каждый четвёртый спринт или около того, выделяя его только для выплаты технического долга.
Плюсы:
Выделенные Capacity для выплаты технического долга
В такой модели agile-команда резервирует определённое количество поинтов или процент от общей capacity спринта для выплаты технического долга на постоянной основе. Например, в каждом спринте команда может брать 5 сторипоинов разнообразной работы по выплате долга.
Плюсы:
Выделение сотрудников на выплату технического долга
Это гибрид двух последних вариантов. В каждом спринте выбирается один разработчик для работы над техническим долгом, в то время как все остальные продолжают заниматься своей обычной работой.
Плюсы:
Выплата технического долга после завершения работы
В такой модели при планировании разработчиками работы они вносят в план подчистку соседнего кода и выплату обнаруживаемого технического долга, уже находящегося в области работы. Это соответствует принципу бойскаутов: всегда оставляй стоянку (кодовую базу) чище, чем она была до тебя.
Иными словами, это подразумевает, что когда ты касаешься кода, он должен становиться лучше. В коде, которого касаются чаще всего, нужно выплачивать наибольший процент по техническому долгу, поэтому логично выплачивать долг в тех областях кода, над которыми ты работаешь.
Похожая концепция приведена в книге Малкольма Глэдуэлла The Tipping Point, где приводится пример с метро Нью-Йорка. Управление городского транспорта выяснило, что если отцеплять вагоны метро, очищать их от граффити и обеспечивать постоянное отсутствие граффити, то можно сэкономить на эффекте разбитых окон, при котором люди считают, что состояние вагонов никого не волнует и уровень преступности может увеличиваться. Сокращая количество «зайцев» и граффити, управление могло также снизить количество насильственных преступлений в метро.
Если перенести тот же принцип на нашу кодовую базу, то нужно сделать так, что при касании областей кода они подчищались и выплачивался технический долг.
Вероятно, из прочитанного выше вы могли догадаться, что я фанат данного подхода, но давайте всё-таки рассмотрим его плюсы и минусы.
Плюсы:
Крупные переработки кода
Выше я рассказывал о стратегии выплаты технического долга путём постепенной замены частей системы, как в мысленном эксперименте с кораблём Тесея, но что если этого недостаточно? Что если у вас нет времени заменять всё ПО часть за частью и вам нужно внести более радикальные изменений?
Вот несколько идей, которые могут вам помочь:
Разбиение приложения на более мелкие приложения
При такой методологии мы разбиваем монолитное приложение на мелкие приложения. Часто такой подход дополняется предметно-ориентированным программированием (Domain Driven Design) и/или микросервисами, но основной его смысл заключается в том, что если приложение слишком объёмно для замены, его можно разделить на меньшие части, замена которых реалистична, после чего можно заменять каждую часть, одну за другой.
Также эту схему можно реализовать с помощью шаблона Strangler Application Мартина Фаулера, при котором создаётся новое приложение, получающее те же запросы, что и старое, и осуществляющее вызовы к легаси-системам, пока для каждой из них не будет готова современная замена.
Плюсы:
Прототипирование приложений в резервной capacity
В такой модели разработчики могут использовать резервное время или время, выделенное на технический долг, чтобы работать над долговременными проектами, например, над заменой всего приложения или его части. Как только будет достигнут достаточный прогресс и можно будет серьёзно приступать к работе, эти задачи начинают внедряться в спринт или в серию спринтов для формальной реализации и поставки.
Действуя по этому шаблону, я очень успешно перенёс JavaScript-приложения на TypeScript, в том числе тратя на это время вне работы (не то, чтобы это было обязательно, но я решил сделать так) и ожидая вывода онлайн сред регрессивного тестирования.
Плюсы:
Полный переход к новому приложению
В этой модели вся работа над старым приложением прекращается, если не считать исправления критичных багов, и начинается работа над приложением, которое станет его полной заменой. Обычно именно это подразумевают люди, когда говорят о переписывании приложения.
Плюсы:
Минусы: