Сравнение эффективности работы команд Проекта Э до и после реинжинирига

July 14, 2023

Введение

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

В изначальных критериях успеха реинжиниринга Проекта Э я целился в то, чтобы как минимум удвоить скорость разработки - лучше и объективно, и субъективно по мнению РП и заказчика. Но субъективные ощущения РП и заказчика не совпадают - РП видит увеличение скорости разработки, а заказчик - нет. Поэтому я перелопатил 517 задач в Jira, чтобы понять объективное состояние дел.

ТЛДР - по статистике количество трудозатрат и багов и правда удалось снизилось в 2 раза. Подробности исследования - далее в посте.

Методика оценки результатов по Jira

Оценка результатов затруднена целым спектром проблем:

  1. О разработке оригинальной версии данных практически нет. Есть только история гита, по которой можно понять размер команды (1 человек) и календарные сроки (9 месяцев - с 13 января - по 8 октября);
  2. Над проектом работали три разные команды - оригинальная, .net-команда у нас и kotlin-команда у нас;
  3. В Jira, откровенно говоря, у нас бардачок. Есть закрытые (и сделанные) задачи без трека. Есть небольшие задачи, в которые затрекано десятки часов.

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

Для этого я навёл порядок в задачах и для каждой проставил/актуализировал:

  1. Тип — задача или баг;
  2. Компонент — .net- или kotlin-back;
  3. метка группировки;
    1. irrelevant — мусорные задачи вроде багов-не багов, которые решились в комментариях;
    2. admin — все возможные дейлики и прочая активность, не связанная с разработкой;
    3. groups — реализация функциональности групп пациентов в старом и новом бэке;
    4. dumps — реализация функциональности выгрузок в старом и новом бэке;
    5. reengineering — реализация функциональности, которая была в проекте на момент окончания разработки изначальной командой;
    6. misc — новые фичи в .net-беке, реинжиниринг этих фич в kotlin-беке, рефакторинг нового бэка (странных частей оригинального бэка, которые при реинжиниринге были сделаны как есть), новые фичи в kotlin-беке, всякая рабочая текучка;
    7. original-bug — баги оригинального бэка.

Эту разбивку задач можно визуализировать так:

tasks decomposition

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

Далее я планирую сравнивать:

  1. Реализацию групп в старом и новом бэке. Это лучшая пара, которая у меня есть - полностью идентичная функциональность, относительно крупная фича и по ней относительный порядок в Jira;
  2. Реализацию выгрузок. Эта фича хороша тем, что она большая и функциональность полностью идентичная. Однако операционные характеристики .net- и kotlin-решения сильно разные, поэтому сравнение не совсем адекватное;
  3. Реализация новых фич и текучка. Проблема этой пары заключается в том, что тут в задачах наибольший бардак с точки зрения трека (где-то его вообще нет - ушёл в соседнюю и т.п.). Но это и самая большая пара по объёму выборки, поэтому, надеюсь, она более-менее адекватно отразит общий тренд;
  4. Реализацию оригинальной версии и её реинжиниринг. Проблема этой пары заключается в том, что по разработке оригинальной версии у меня нет подробных данных, а есть только история гита. Однако по трудозатратам это самая большая пара, поэтому надеюсь что она тоже более-менее адекватно отразит общий тренд.

Данные

Реализация групп

Это небольшая фича по управлению собственно группами пациентов, наблюдаемых одним или более врачом. Не совсем тривиальная, из-за отношения many-to-many с относительно большой кардинальностью (до сотен строк).

  1. Старый бэк
    1. Общие трудозатраты: 104 часа
    2. Количество багов: 4
  2. Новый бэк
    1. Общие трудозатраты: 58
    2. Количество багов: 2

Здесь мы видим сокращение трудозатрат в 1.8 раз и сокращение багов в 2 раза, хотя по багам объём выборки не репрезентативен.

Реализация выгрузок

Это небольшая ридонли-админка пациентов и событий их дневников, в которой функционально всё стандартно - таблички на 10 столбцов, фильтрация по ним, сортировка, пагинация, выгрузка в xlsx. Все сложности были заключены в том, что данные лежат в разных БД. И в случае событий в перспективе ожидаются сотни миллионов строк.

  1. Старый бэк
    1. Общие трудозатраты: 247 часа
      1. Реализация: 179
      2. Исправление багов: 68
    2. Количество багов: 21
  2. Новый бэк
    1. Общие трудозатраты: 175.75
      1. Реализация: 172
      2. Исправление багов: 3.75
    2. Количество багов: 4

Тут мы видим сокращение трудозатрат в 1.4 раза и сокращение количества багов в 5 раз.

Однако напрямую эти две реализации сравнивать нельзя, потому что здесь на новом бэке мы сделали выборки на потоковых join-ах. Благодаря чему новый бэк может обработать 1М строк (старый - 64К), а 64К строк сгенерировать за 11 секунд (старый - 165 секунд).

Кроме того, старый бэк полностью покрыт тестами.

Реализация новых фич и текучка

  1. Старый бэк
    1. Общие трудозатраты: 526 часа
      1. Реализация: 353.5
      2. Исправление багов: 172.5
    2. Количество задач: 14
    3. Количество багов: 22
    4. Медианные трудозатраты на задачу: 16
  2. Новый бэк
    1. Общие трудозатраты: 497 часа
      1. Реализация: 426
      2. Исправление багов: 71
    2. Количество задач: 52
    3. Количество багов: 24
    4. Медианные трудозатраты на задачу: 5

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

Тем не менее по всем метрикам наблюдается положительный тренд:

  1. За сопоставимый объём часов было выполнено в 3 раза больше задач;
  2. Относительное количество багов (22/14 vs 24/52) также уменьшилось в 3 раза;
  3. Наконец, медианные трудозатраты на задачу тоже снизились в три раза.

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

Тем не менее я субъективно оцениваю, что в старом бэке была сделана только одна более-менее крупная задача (на 90 часов), а в остальном это были мелкие допилы и фиксы, которые занимали огромное количество времени.

Также субъективно, я оцениваю, что в kotlin-бэке было сделано четыре задачи, аналогичных по сложности самой большой задаче .net-бэка, плюс два крупных рефакторинга (100 и 13 часов).

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

Реализация оригинальной версии и её реинжиниринг

  1. .net-бэк
    1. Оценочные общие трудозатраты: 1512
  2. kotlin-бэк
    1. Общие трудозатраты: 1162
      1. Реализация: 852
      2. Исправление багов: 59
      3. Административные задачи: 251

Тут ускорение разработки составляет 1.3 раза. Однако здесь мы сравниваем наименее однородные вещи:

  1. У kotlin-команды было преимущество в фиксированном и проработанном "ТЗ". Однако "ТЗ" - это исходный и местами запутанный код на незнакомом языке;
  2. kotlin-бэк делали два юниора и стажёр, а .net-бек - один человек, и поэтому полагаю, что как минимум формально это был мидл или сеньёр;
  3. Оригинальному разработчику приходилось проектировать решение, а kotlin-команде приходилось подстраиваться под это решение, которое не всегда хорошо ложилось на наш стэк, а местами было очень странным;
  4. Оригинальный разработчик тесты не писал, а у kotlin-команды было 100% покрытие тестами хэппи пасов и 90% покрытие строк кода;

Итоги

Итак, у меня есть:

  1. Данные по полностью идентичной реализации одной и той же функциональности объёмом в 1-2 недели - почти в два раза быстрее и в два раза меньше багов;
  2. Данные по объективно более качественной реализации одной и той же функциональности объёмом в 1-1.5 месяца - в полтора раза быстрее и в пять раз меньше багов;
  3. Данные по 3 месяцам работы над преимущественно разными задачами - за примерно одинаковое время и с примерно одинаковым количеством багов, kotlin-команда сделала в три раза больше задач.

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

Я считаю, это очень хороший результат и цель "как минимум двойное сокращение трудозатрат и багов" можно с уверенностью считать достигнутой. Но что позволило достичь этой цели?

Гипотезы причин улучшений

На итоговые цифры повлияли как минимум следующие факторы:

  1. Переход с микросервисов на монолит;
  2. Разные люди;
  3. Покрытие кода тестами;
  4. Переход с вертикальной на функциональную архитектуру;
  5. Разные стеки.

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

Переход с микросервисов на монолит

На мой взгляд, наибольший вклад в увеличение скорости разработки внёс переход на монолит. Пусть он будет ответственен за 32% улучшения. Из цифр видно, что версию на монолите сделали на 20-30 процентов быстрее (смотря что на что делить). И я думаю, что это консервативная оценка - если бы kotlin-версию делал также один мидл, то он сделал бы в два раза/на 50% быстрее. По крайней мере, для себя я сделал вывод, что делать проекты до человеко/года на микросервисах как минимум в два раза дороже, чем на монолите.

Разные люди

Далее идёт самый сложный фактор на мой взгляд - люди. По моей оценке вклад смены команды в увеличение скорости разработки составляет 31%.

Про оригинального разработчика я не знаю ничего, но с учётом довольно небольшой разницы между оригинальными трудозатратами и трудозатратами на реинжиниринг, могу предположить, что квалификация и мотивация оригинального разработчика примерно соответствовала kotlin-команде (я помню, что предположил, что это был как минимум мидл, но там была и оговорка: "как минимум формально").

А вот с .net-командой я зафакапился тотально. У меня там были все - и юниор, и мидл, и сеньёр, и техлид. Все, кроме юниора, имели свой грейд чисто формально. Поэтому всех их (кроме юниора) я быстро уволил (от двух недель до двух месяцев) за низкую эффектиность работы.

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

Покрытие кода тестами

Теперь, наоборот, самый простой фактор — покрытие тестами. Его вклад в сокращение багов — 100%, на мой взгляд. Если бы kotlin-команада работала без тестов, то багов было бы столько же, сколько и у .net-команды.

Касательно увеличения скорости разработки, то по цифрам выходит, что вклад тестов составляет 15% - в .net-беке на исправление багов уходило 30%, а в kotlin - 15% (это в новых фичах и поддержке, а в выгрузках - вообще - 2%). Но исходя из гипотезы, что тесты влияют на мотивацию, а также из тех соображений, что баги несут очевидный и серьёзный репутационный (а иногда и материальный ущерб) - вклад покрытия тестами я оцениваю на том же уровне, что и переход на монолит и смену команды - 30%.

Переход с вертикальной на функциональную архитектуру

Теперь к смене вертикальной архитектуры на функциональную. Я думаю, что этот фактор именно с точки трудозатрат на кодирование имел небольшое влияние — в лучшем случае 7%. Зато вкупе с отсутствием тестов, он имел серьёзное влияние на количество багов — я не стал тут уже закапываться в статистику, но в .net-беке у нас не раз были баги из серии "Тут SQL поправили, а в соседней директории — забыли".

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

Разные стеки

Если вы следите за цифрами, то уже знаете, что вклад смены стека я оцениваю в 0%. На мой взгляд, между Kotlin и C# никакой существенной разницы нет. И при прочих равных, что изначальная разработка на Kotlin, что реинжинриниг на C# дали бы те же самые результаты.

Итоги

По моей оценке вклад факторов в результат следующий:

  1. Переход с микросервисов на монолит - 32%;
  2. Разные люди - 31%;
  3. Покрытие кода тестами - 30%;
  4. Переход с вертикальной на функциональную архитектуру - 7%;
  5. Разные стеки - 0%.

При чём здесь Эргономичный подход?

Помимо вопроса "стоило ли оно того в целом", меня ещё интересует вопрос "стоило ли проводить реинжиниринг по Эргономичному подходу"? Данных, чтобы дать обоснованный ответ, у меня нет, так как сравнивать не с чем, но пофантазировать всё-таки хочется.

Что бы было, если бы мы делали реинжиниринг по мейнстримному подходу - с тестами на моках, Hibernate, пакетированием по техническим аспектам и в императивном стиле?

Сравнивать kotlin-бэк с гипотетический мейнстримным бэком я в том же формате, что и с .net-бэком.

Реализация групп

Я думаю, что использование Hibernate и тестов на моках, позволило бы сократить трудозатраты на 10-30% и, возможно, несущественно бы увеличило количество багов.

  1. Гипотетический мейнстримный бэк
    1. Оценочные общие трудозатраты: 41-52 часа (<58 часов факта ЭП-версии> - 10-30%)
    2. Оценочное количество багов: 2-3 штуки (<2 бага факта ЭП-верисии> + 0-1 шт.)

Реализация выгрузок

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

А в силу того, что в реализации много "юнитов" и у них много зависимостей, тесты на моках и сами стоили бы дороже, и багов больше бы пропустили. И, как следствие, ещё больше увеличили бы общие трудозатраты. В итоге, я думаю, получилось бы +10% к трудозатратам на тесты и 30% на фикс багов.

  1. Гипотетический мейнстримный бэк
    1. Оценочные общие трудозатраты: 245.9
      1. Реализация: 189.2 (<172 часов факта ЭП-версии> + 10%)
      2. Исправление багов: 56.7 (30% от 189.2)
    2. Оценочное количество багов: 13 (с потолка)

Реализация новых фич и текучка

В эту категорию попадают уже в основном доработки существующей функциональности и рефакторинг. И тут (по идее) должен начать проявляться эффект от применения ЭП. С точки зрения сцепленности продового кода, негативные эффекты мейнстримного подхода ещё не успели бы проявиться. А вот в тестах - уже бы проявились в полный рост. В итоге, я полагаю, трудозатраты на реализацию бы выросли на 10-20% (на актуализацию моков), а трудозатраты на исправление багов, пропущенных тестами на моках, выросли бы до 20-25%.

  1. Гипотетический мейнстримный бэк
    1. Общие трудозатраты: 562.2-585.7 часа
      1. Реализация: 468.6 (<426 часов факта ЭП-версии> + 10%)
      2. Исправление багов: 93.7-117.1 (20-25% от 468.6)
    2. Количество задач: — не знаю, как хоть сколько-нибудь адекватно оценить и выровнять с общими трудозатратами
    3. Количество багов: — не знаю, как хоть сколько-нибудь адекватно оценить и выровнять с общими трудозатратами
    4. Медианные трудозатраты на задачу: 5.5-6 (5 + 10-20%)

Реализация оригинальной версии и её реинжиниринг

При выполнении реинжиниринга, за счёт использования Hibernate трудозатраты на реализацию сократились бы процентов на 20 и ещё процентов на 10 за счёт тестов на моках. С другой стороны, трудозатраты на исправление багов удвоились бы за счёт багов, пропущенных тестами на моках. Наконец, административные трудозатраты не изменились бы.

  1. Гипотетический мейнстримный бэк
    1. Общие трудозатраты: 965.4
      1. Реализация: 596.4 (70% от 852 часов факта ЭП-версии)
      2. Исправление багов: 118 (59 часов факта ЭП-версии + 100%)
      3. Административные задачи: 251

Итого

Итого общие трудозатраты на "первые две версии" (реинжиниринг и 3 месяца саппорта) по ЭП составили 2039 часов. А оценочные общие трудозатраты на "первые две версии" по мейнстримному подходу составили бы 1814.5-1849.

То есть первый год разработки по ЭП будет примерно на 10% дороже.

Однако, как показывает моя практика, при разработке по мейнстримному подходу, трудо- и баго-ёмкость задач растёт очень быстро.

В случае же ЭП, предположительно, они будут расти намного медленнее.

Это я и собираюсь проверить - я надеюсь, Проект Э проживёт ещё хотя бы пару лет (все предпосылки для этого есть) и я смогу ещё хотя бы три-четыре раза с интервалом в 3-6 месяцев повторить это упражнение и оценить тренд роста трудозатрат и количества багов на задачу при работе с эргономичной кодовой базой.

Выводы

Итак: стоило ли делать реинжиниринг? Безусловно да, на основе данных из Jira можно с уверенностью утверждать, что мы смогли снизить трудозатраты и количество багов как минимум в два раза. Это улучшение ещё "усугубляется" за счёт того, что для заказчика внешние рейты штатных kotlin-истов юниоров ниже внешних рейтов .net-чиков аутстафферов.

Стоило ли делать реинжинирнг по Эргономичному подходу? Доподлинно неизвестно. Гипотетически, при условии, что работы продолжатся ещё хотя бы год, и если я прав, что показатели будут деградировать заметно медленнее, чем по мейнстримному подходу - да. Но это всё теория, которую ещё предстоит проверить на практике.

Кроме того, результаты анализа дают дополнительное подтверждение следующим утверждениям:

  1. Первый год разработки на микросервисах дороже разработки на монолите. Минимум на 30%;
  2. Автоматизация тестирования снижает количество багов и трудозатрат на их устранение. Минимум в два раза;
  3. Мотивация команды имеет огромное влияние на трудозатраты. От 30% дополнительных трудозатрат в случае низкой мотивации.