среда, 18 мая 2011 г.

Технический долг

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

При этом самым сложным при выборе того или иного решения является «коммуникация» своего выбора непосредственному руководителю, чтобы он смог принять взвешенное решение. А поскольку с точки зрения большинства руководителей «взвешивание» заканчивается сразу же после того, как он услышит сроки реализации, то «коммуникация» заканчиваются примерно через 37 секунд после ее начала (обычно именно столько времени нужно руководителю, чтобы узнать ответ на очень простой вопрос, выражаемый одним словом: «Когда?»)

Не удивительно, что многие простые программисты, матерые лиды и архитекторы, а иногда даже ПМ-ы, которые понимают, что им самим придется расхлебывать проблемы «близоруких» решений, с таким подходом не согласны. И совершенно не удивительно, что с подобной проблемой сталкивались и другие известные и не очень люди, которые придумали типовые «паттерны», описывающие подобную ситуацию. Одним из таких паттернов, является метафора технического долга, впервые описанная Вардом Каннингемом (Ward Cunningham) (*) без малого двадцать лет назад.

Технический долг в классическом понимании

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

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

У этой метафоры есть одна очень важная особенность: когда речь идет о неоптимальном решении, речь идет об использовании .NET Remoting вместо WCF, использовании Sybase, вместо SQL Server, использовании DataSet-ов вместо Entity Framework (**), однако никто не говорит о «забивании костылей», грязных хаках, плохом коде, связной архитектуре и тому подобном. Как раз наоборот, неоптимальность стратегического решения не означает, что к нему нужно относится спустя рукава, распространить влияние этого решения на всю систему или просто напросто г#$нокодить. Как раз, наоборот, в большинстве случаев это означает, что это решение должно быть спрятано в виде детали реализации, одного или по крайней мере, как можно меньшего числа модулей, чтобы его можно было изменить позднее.

Именно такой подход к построению приложений является наиболее оптимальным с архитектурной точки зрения. Если принимая архитектурное решение вы не чувствуете компромисса между краткосрочными и долгосрочными выгодами, то в любом случае не стоит вырубать его в камне. Вполне возможно, что вы приняли неправильное решение неосознанно, а из-за своего непонимания предметной области, или из-за будущего изменения требований заказчиком. Значительно проще снизить стоимость будущих изменений путем инкапусуляции важных решений в наименьшее число модулей, нежели стараться продумать архитектуру до мельчайших подробностей, в надежде учесть все возможные и невозможные сценарии. Для определенного круга задач идеальная продуманная архитектура возможна, а иногда просто необходима, но в большинстве случаев – это не так; рано или поздно придет момент, когда ваше видение системы или видинение системы заказчиком изменится, что потребует внесения в нее существенных изменений.

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

Грязный код, как источник технического долга

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

Во время кодирования, как и во время принятия любых других решений, разработчик должен рассматривать краткосрочные и долгосрочные выгоды. Давайте рассмотрим юнит-тесты. Если спросить адепта 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, в котором Кент Бек как раз и высказал эту заветную мысль.

7 комментариев:

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

    ОтветитьУдалить
  2. Вот как раз эта метафора для этого и предназначена. С помощью нее, а не с помощью совершенно непонятных понятий типа "сопровождаемости" гораздо легче объяснить нетехнорю-заказчику о том, в чем именно заключается компромисс и какие плюсы и минусы он получает в обоих случаях.

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

    ОтветитьУдалить
  3. А переписывание всего заново, это реструктуризация долга, когда команда разработчиков "обанкротилась". :)

    ОтветитьУдалить
  4. Мне кажется за бортом остался вопрос "выращивания" заказчика. Как правило, заказчика интересует здесь и сейчас. Для того, чтобы заказчик понимал, почему разработчики определяют временные требования. Например, у нас были случаи, когда директоров возили на семинары по Agile. В этом случае не возникают (или возникают но в меньшей мере)вопросы - почему так долго. Вообще, хорошая статья. Особенно сделан упор на "почему". Хотя вопрос "как избежать" не освещен(возможно этого и не задумывалось).

    ОтветитьУдалить
  5. @eugene: Да, цель статьи - это прежде всего описать типовую ситуацию, с которой сталкивается большинство проектов, ну а что делать потом с этим делом - это вопрос второй. Тут как всегда главное понять, что проблема есть и какие существуют компромисы для ее решения, а остальное - уже дело техники:)

    ОтветитьУдалить
  6. Уныло как-то осознавать эту проблему. Наверно, у проблемы есть еще какие-то решения. Может не надо делать одно огромное ПО, а сделать несколько маленьких?

    ОтветитьУдалить
  7. Далеко не всегда можно избежать создания одного большого приложения, заменив его десятком маленьких. Да и тогда останется груз нерешенных проблем и мелких ошибок, которые возникнут при взаимодействии этих приложений.
    Так что решение проблемы сводится к наиболее простому и гибкому дизайну, когда в системе существует как можно меньше решений вырубленных в камне и от которых невозможно отказаться позднее. Кроме того - знание - сила. Так что если команда знает об этой проблеме, появляются хотя бы какие-то шансы на ее решение (или по крайней мере на смягчение последствий).

    ОтветитьУдалить