Микроретро Проекта Э

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

Как я буду оценивать результаты

Для меня успех выглядит так: РП и заказчик отвечают утвердительно на вопрос "Видите ли вы на глаз как минимум удвоение скорости разработки?". Понятно, что это субъективно, но это именно то, что я продаю.

Кроме того, я посмотрю кол-во багов и регрессий, а так же среднее время задачи от "In Progress" до "Done" за месяц работы "до" и "после".

Если РП и клиент заметят удвоение на глаз и оно подтвердится цифрами - это будет оглушительный успех

Если РП и клиент не заметят удвоения, а на цифрах оно будет - пойду учиться доносить результаты

Если наоборот - можно так и оставить 😂

Если РП и клиент не заметят удвоения и это не подтвердится цифрами - ну всё, расходимся

Субъективные ощущения РП и заказчика противоречат друг другу - РП субъективно видит увеличение скорости разработки, а заказчик - нет.

Поэтому я перелопатил 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-бэке было сделано четыре аналогичных по сложности фичи и 2 крупных рефакторинга (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-команада работала без тестов, то багов было бы столько же.

Касательно увеличения скорости разработки, то по цифрам выходит, что вклад тестов составляет 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% дополнительных трудозатрат в случае низкой мотивации.