Анатомия техдолга. Излечим ли пациент?

Привет, Хабр! Меня зовут Евгений Старков, и я, как и вы, часто сталкиваюсь с техдолгом.

Пришло время рассказать, как я с ним справляюсь! Проблема не нова и встречается в любой компании, связанной с разработкой. Мне, по большей части, техдолг достался «в наследство». Первой задачей в Тензоре было разделение и рефакторинг большой части кодовой базы своей группы. Там попадались и суперклассы, и файлы с количеством строк, перевалившем далеко за 1000, и именование переменных по типу a, b, c.

Когда увидел кодовую базу, которая теперь твоя

Когда увидел кодовую базу, которая теперь твоя

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

Как я себя чувствовал, когда занимался данной задачей

Как я себя чувствовал, когда занимался данной задачей

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

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

  1. сам код (качество, стиль, чистота)

  2. исполнение в рантайме (скорость, оптимальность)

Соберем анамнез. Посмотрим, как было и потом сравним, спасли мы пациента или наоборот, сделали так, что он быстрее уйдет на покой.

Распилим пациента и посмотрим, что у него внутри

Распилим пациента и посмотрим, что у него внутри

Начнем с кода. Как выявить болячки здесь? Проведем диагностику. А в этом нам помогут линтеры, для предварительного диагноза. Начнем с pylint и проверим качество написания кода:

b21de31852e7bcd6e3fc9cbbaa7c6a13.png

Плохо, но не критично. Тут в файлах заметно, что часть симптомов решили скрыть, отключив линтер:

14a8c6dd73cf35a69fa2a2d31fecca59.png

Мы хотим знать объективную правду, срываем маски и видим, что пока сделали только хуже:

ee9d73097f0f1ad8d450449ac0b24434.png

Начнем лечение того, что выявили:

  1. Один файл больше 1000 строк (1847). Решим немного позже, так как можем полечить его, избавившись от других.

f59c0cd6d6278068509efdd90682157c.png

  1. Дублирование кода, порой даже в пределах 100 строк. Вот пример:

adebca85e1b27db5ff84779e4ec7c838.png

Плюсом еще такое условие в 2х местах. Выносим в функцию и заменяем:

Создали отдельную функцию

Создали отдельную функцию

И используем уже ее

И используем уже ее

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

1f818a5c3f27344d00a1ca564efad7e1.png

Результатом трудов стало улучшение состояния — мы делаем коду лучше.

a0d2b753d889b387673aae198bf7d4c7.png

  1. Следующая проблема — использование форматирования строки, вместо f-строк. Данная проблема появилась по причине развития языка и ярко отображает то, что идеальный код со временем может приобрести проблемы. А таких у нас 87.

4da103b941fccdc78720c3019886a3a3.png

Эти ошибки исправить просто — меняем формат на f-строку.

791c93c4838d66f6cf3a6bdccefa9f7a.png

  1. Константы, шаблоны запросов и исключения затеряны в самом коде.

bc98511f9ebb5c2142ec6bc405b273c2.png2cc9404f2934ead4ae992c3aa34ecfed.png

Это уменьшает читаемость. Все выносим в отдельные файлы.

  1. Большое количество ветвлений, принимаемых и неиспользуемых аргументов.

2c63608ffc10ca089c21d29fa0a7e828.pngcdf0381aaaff194b6af656c8690057a7.png

Большие функции на 200+ строк. По возможности избавляемся от ветвлений: одну из функций пришлось сделать классом (чтобы уменьшить ветвление), упростить сам код и сделать его лаконичней.

Теперь наш код здоров:

08c4fba20d2b05c5d36de951f855852b.pngКак я себя чувствовал

Как я себя чувствовал

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

e9e1b2c06e085e57d8d06f338da47a00.png

Поэтому назначаем еще одну диагностику, а именно mypy, и видим следующую картину:

adb62e51347f57015d571c618b543f08.png

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

Теперь и читать приятно, и mypy не ругается

Теперь и читать приятно, и mypy не ругается

А ты юзаешь подсказки типов?

А ты юзаешь подсказки типов?

Но откуда же взялись эти проблемы?

Основные причины появления технического долга:

  1. низкая квалификация сотрудников и отсутствие надзора за их исполнением;

  2. отсутствие стандартов написания кода, проектирования функционала;

  3. отсутствие контроля за соблюдением правил, стилистики и архитектуры;

  4. жесткие временные рамки;

  5. плохая взаимосвязь или ее отсутствие между бизнесом и разработкой;

  6. ошибки проектирования или его отсутствие;

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

Теперь само написание кода проблем не имеет, но пациент все равно нам всеми силами намекает:»что — то не так!». Простая задачка им выполняется слишком долго, как будто он делает много лишнего.

Идем смотреть, как все работает и видим эти симптомы.

4e95ca99b7e63d5589094e1a13b64933.png

Наш запрос выполняется очень долго, больше половины времени работы всего функционала. Смотрим план, ищем проблему. Оказалась, что нам нужно прочитать несколько записей (часто не больше 10), а мы вычитывали всю базу с кучей Join`ов и фильтраций. Добавляем условие для фильтрации по конкретным идентификаторам и получаем:

После правок SQL запроса

После правок SQL запроса

То есть прирост составил больше 95% во времени и сам запрос теперь занимает не больше 10% тайминга работы всего метода.

Но теперь у нас есть трата временного ресурса на работу языка, занимающая больше 500 мс или почти 70% рабочего времени.

Начинаем смотреть и видим не оптимальность.

3050f595ff22f8bc089da29f925dabfe.png

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

Пример локальный, но хорошо демонстрирует суть оптимизации

Пример локальный, но хорошо демонстрирует суть оптимизации

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

b0aa6fe530b72cad2af192a08eb4dac4.png

Теперь среднее время работы составило не 1800 мс, а всего лишь 160 мс. То есть мы получили прирост в скорости больше чем на 90%.

Пациент спасен и теперь гордо трудится на радость клиентам, но таких как он, еще много.

Я справился!

Я справился!

Почти все ИТ-компании имеют технический долг, но не у каждой есть с ним сложности. Где же эта тонкая грань?

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

  1. Большие временные затраты

  2. Сложность внедрения новыx фич, которые могут увеличить сам долг.

  3. Снижение мотивации команды и общей комфортной среды внутри.

  4. Выгорание и отсутствие интереса.

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

Существуют и другие способы уменьшения долга:

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

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

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

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

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

Подведем итоги

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

Вот и всё. Буду рад здравой критике и ярым обсуждениям.

© Habrahabr.ru