Микроретро Проекта Э
Пост с ретро Проекта Э идёт со скрипом. Преимущественно потому, что последние несколько недель я искал ответ на вопрос "стоило ли делать реинжиниринг". Напомню, что в изначальных критериях успеха этой авантюры я целился в то, чтобы как минимум удвоить скорость разработки:
Как я буду оценивать результаты
Для меня успех выглядит так: РП и заказчик отвечают утвердительно на вопрос "Видите ли вы на глаз как минимум удвоение скорости разработки?". Понятно, что это субъективно, но это именно то, что я продаю.
Кроме того, я посмотрю кол-во багов и регрессий, а так же среднее время задачи от "In Progress" до "Done" за месяц работы "до" и "после".
Если РП и клиент заметят удвоение на глаз и оно подтвердится цифрами - это будет оглушительный успех
Если РП и клиент не заметят удвоения, а на цифрах оно будет - пойду учиться доносить результаты
Если наоборот - можно так и оставить 😂
Если РП и клиент не заметят удвоения и это не подтвердится цифрами - ну всё, расходимся
Субъективные ощущения РП и заказчика противоречат друг другу - РП субъективно видит увеличение скорости разработки, а заказчик - нет.
Поэтому я перелопатил 517 задач в Jira, чтобы понять объективное состояние дел. ТЛДР - по статистике количество трудозатрат и багов и правда удалось снизилось в 2 раза. Подробности исследования - далее в посте.
Методика оценки результатов по Jira
Оценка результатов затруднена целым спектром проблем:
- О разработке оригинальной версии данных практически нет. Есть только история гита, по которой можно понять размер команды (1 человек) и календарные сроки (9 месяцев - с 13 января - по 8 октября);
- Над проектом работали три разные команды - оригинальная, .net-команда у нас и kotlin-команда у нас
- В Jira, откровенно говоря, у нас бардачёк; Есть закрытые (и сделанные) задачи без трека; Есть "небольшие" задачи на десятки часов.
Тем не менее, я постарался сделать максимум для того чтобы "сравнивать апельсины с апельсинами" и получить более-менее адекватный результат.
Во-первых, я навёл порядок в задачах и для каждой проставил/актуализировал:
- Тип - задача или баг
- Компонент - .net- или kotlin-back
- метка группировки
- irrelevant - мусорные задачи вроде багов-не багов, которые решились в комментах
- admin - все возможные дейлики и прочая активность, не связанная с разработкой
- groups - реализация функциональности групп пациентов в старом и новом бэке
- dumps - реализация функциональности выгрузок в старом и новом бэке
- reengineering - реализация функциональности, которая была в проект на момент приёмки
- misc - новые фичи в .net-беке, реинжиниринг этих фич в kotlin-беке, рефакторинг нового бэка (странных частей оригинального бэка, которые при реинжиниринге были сделаны как есть), новые фичи в kotlin-беке, всякая рабочая текучка
- original-bug - баги оригинального бэка
Эту разбивку задач можно визуализировать так:
Здесь цветами закодированы группы задач, которые я планирую сравнивать между собой, а серые группы - задачи которые я исключил из сравнения.
Далее я планирую сравнивать:
- Реализацию групп в старом и новом бэке. Это лучшая пара, которая у меня есть - полностью идентичная функциональность, относительно крупная фича и по ней относительный порядок в Jira.
- Реализацию выгрузок. Эта фича хороша тем, что она большая и функциональность полностью идентичная. Однако операционные характеристики .net- и kotlin-решения сильно разные, поэтому сравнение не совсем адекватное
- Реализация новых фич и текучка. Проблема этой пары заключается в том, что тут в задачах наибольший бардак с точки зрения трека (где-то его вообще нет - ушёл в соседнюю и т.п.). Но это и самая большая пара по объёму выборки, поэтому надеюсь она более-менее адекватно отразит общий тренд.
- Реализацию оригинальной версии и её реинжиниринг. Проблема этой пары заключается в том, что по разработки оригинальной версии у меня нет подробных данных, а есть только история гита. Однако по трудозатратам это самая большая пара, поэтому надеюсь что она тоже более-менее адекватно отразит общий тренд
Данные
Реализация групп
Это небольшая фича по управлению собственно группами пациентов, наблюдаемых одним или более врачем. Не совсем тривиальная, из-за отношения many-to-many с относительно большой кардинальностью (до сотен строк).
- Старый бэк
- Общие трудозатраты: 104 часа
- Количество багов: 4
- Новый бэк
- Общие трудозатраты: 58
- Количество багов: 2
Здесь мы видим сокращение трудозатрат в 1.8 раз и сокращение багов в 2 раза, хотя по багам объём выборки не репрезентативен.
Реализация выгрузок
Это небольшая ридонли-админка пациентов и событий их дневинков, в которой функционально всё стандартно - таблички на 10 столбцов, фильтрация по ним, сортировка, пагинация, выгрузка в xlsx. Все сложности были заключены в том, что данные лежат в разных БД. И в случае событий в перспективе ожидается сотни миллионов строк.
- Старый бэк
- Общие трудозатраты: 247 часа
- Реализация: 179
- Исправление багов: 68
- Количество багов: 21
- Общие трудозатраты: 247 часа
- Новый бэк
- Общие трудозатраты: 175.75
- Реализация: 172
- Исправление багов: 3.75
- Количество багов: 4
- Общие трудозатраты: 175.75
Тут мы видим сокращение трудозатрат в 1.4 раза и сокращение количества багов в 5 раз.
Однако напрямую эти две реализации сравнивать нельзя, потому что здесь на новом бэке мы сделали выборки на тех самых потоковых join-ах.
Благодаря потоковым джоинам новый бэк может обработать 1М строк (старый - 64К), а 64К строк сгенерировать за 11 секунда (старый - 165 секунд).
Кроме того, старый бэк полностью покрыт тестами.
Реализация новых фич и текучка
- Старый бэк
- Общие трудозатраты: 526 часа
- Реализация: 353.5
- Исправление багов: 172.5
- Количество задач: 14
- Количество багов: 22
- Медианные трудозатраты на задачу: 16
- Общие трудозатраты: 526 часа
- Новый бэк
- Общие трудозатраты: 497 часа
- Реализация: 426
- Исправление багов: 71
- Количество задач: 52
- Количество багов: 24
- Медианные трудозатраты на задачу: 5
- Общие трудозатраты: 497 часа
Сравнение этих метрик уже с большой натяжкой можно назвать объективным, потому что здесь у нас на входе по большей части разные задачи, выполненные разными людьми.
Тем не менее по всем метрикам наблюдается положительный тренд:
- За сопоставимый объём часов было выполненло в 3 раза больше задач
- Относительное количество багов (22/14 vs 24/52) так же уменьшилось в 3 раза
- Наконец, медианные трудозатраты тоже снизились в три раза
Тут можно сказать, что это просто выборка такая и на новом беке в среднем делали в три раза более простые задачи. И на самом деле тут дать объективную оценку сложно, потому что непонятно как объективно оценивать сложность задач.
Тем не менее, я субъективно оцениваю, что в старом бэке была сделана только одна более-менее крупная задача (на 90 часов), а в остальном это были мелкие допилы и фиксы, которые занимали огромное количество времени.
Так же субъективно, я оцениваю что в kotlin-бэке было сделано четыре аналогичных по сложности фичи и 2 крупных рефакторинга (100 и 13 часов).
В общем и целом я думаю, что этот блок можно считать подтверждением того, что разработка на новом бэке требует как минимум в два раза меньше трудозатрат и порождает как минимум в два раза меньше багов.
Реализация оригинальной версии и её реинжиниринг
- .net-бэк
- Оценочные общие трудозатраты: 1512
- kotlin-бэк
- Общие трудозатраты: 1162
- Реализация: 852
- Исправление багов: 59
- Административные задачи: 251
- Общие трудозатраты: 1162
Тут ускорение разработки составляет 1.3 раза. Однако здесь мы сравниваем наименее однородные вещи:
- У kotlin-команды было преимущество в фиксированном и проработанном "ТЗ". Однако "ТЗ" - это исходный и местами запутанный код на незнакомом языке;
- kotlin-бэк делали три юниора, а .net-бек - один человек, и по этому полагаю, что как минимум формально это был как минимум мидл;
- Оригинальному разработчику приходилось проектировать решение, а kotlin-команде приходилось подстраиваться под это решение, которое не всегда хорошо ложилось на наш стэк, а местами было очень странным;
- Оригинальный разработчик тесты не писал, а у kotlin-команды было 100% покрытие тестами хэппи пасов и 90% покрыте строк кода;
Итоги
И так, у меня есть:
- Данные по полностью идентичной реализации одной и той же функциональности объёмом в 1-2 недели - почти в два раза быстрее и в два раза меньше багов;
- Данные по объективно более качественной реализации одной и той же функциональности объёмом в 1-1.5 месяца - в полтора раза быстрее и в пять раз меньше багов;
- Данные по 3 месяцам работы над преимущественно разными задачами - за примерно одинаковое время и с примерно одинаковым количеством багов, kotlin-команда сделала в три раза больше задач.
Исходя из этих данных я делаю следующий вывод - затратив 82% оригинальных трудозатрат команда юниоров смогла создать базу проекта, который по самой консервативной оценке в два раза быстрее разрабатывать и содержит как минимум в два раза меньше багов.
Я считаю, это очень хороший результат и цель "как минимум двойное сокращение трудозатрат и багов" можно с уверенностью считать достигнутой. Но что позволило достичь этой цели?
Гипотезы причин улучшений
На итоговые цифры повлияли как минимум следующие факторы:
- Переход с микросервисов на монолит;
- Разные люди;
- Покрытие кода тестами;
- Переход с вертикальной на функциональную архитектуру;
- Разные стеки.
И как их расцепить и точно определить вклад каждого фактора я не знаю. Но попробую передать своё субъективное ощущение. Спойлер - список выше отсортирован по убыванию вклада.
Переход с микросервисов на монолит
На мой взгляд, наибольший вклад в увеличение скорости разработки внёс переход на монолит. Пусть он будет ответственен за 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# дали бы те же самые результаты.
Итоги
Итого, по моей оценке вклад факторов в результат следующий:
- Переход с микросервисов на монолит - 32%;
- Разные люди - 31%;
- Покрытие кода тестами - 30%;
- Переход с вертикальной на функциональную архитектуру - 7%;
- Разные стеки - 0%.
При чём здесь Эргономичный подход?
Помимо вопроса "стоило ли оно того в целом", меня ещё интересует вопрос "стоило ли проводить реинжиниринг по Эргономичному подходу"? Данных, чтобы дать обоснованный ответ, у меня нет, но пофантазировать всё-таки хочется.
Чтобы было бы, если бы мы делали реинжиниринг по мейнстримному подходу - с тестами на моках, Hibernate, пакетированием по техническим аспектам и в императивном стиле?
Сравнивать kotlin-бэк с гипотетический мейнстримным бэком я в том же формате, что и с .net-бэком.
Реализация групп
Я думаю, что использование Hibernate и тестов на моках, позволило бы сократить трудозатраты на 10-30% и, возможно, несущественно бы увеличило количество багов.
- Гипотетический мейнстримный бэк
- Оценочные общие трудозатраты: 41-52 часа (58 часов факта ЭП-версии - 10-30%)
- Оценочное количество багов: 2-3 штуки (2 бага факта ЭП-верисии + 0-1 шт.)
Реализация выгрузок
Реализация выгрузок миллионов строк на базе Hibernate наверняка привела бы к деградации потребления памяти и скорости работы. Поэтому для сохранения качества реализации, выгрузки пришлось в любом случае делать на JdbcTemplate-е. По крайней мере я даже в работе по мейнстримному подходу сделал бы выгрузку точно так же.
А силу того, что в реализации много "юнитов" и у них много зависимостей, тесты на моках и сами стоили бы дороже, и багов больше бы пропустили. И, как следствие, ещё больше увеличили бы общие трудозатраты. В итоге, я думаю, получилось бы +10% к трудозатратам на тесты и 30% на фикс багов.
- Гипотетический мейнстримный бэк
- Оценочные общие трудозатраты: 245.9
- Реализация: 189.2 (172 часов факта ЭП-версии + 10%)
- Исправление багов: 56.7 (30% от 189.2)
- Оценочное количество багов: 13 (с потолка)
- Оценочные общие трудозатраты: 245.9
Реализация новых фич и текучка
В эту категорию попадают уже в основном доработки существующей функциональности и рефакторинг. И тут (по идеи) должен начать проявляться эффект от применения ЭП. С точки зрения сцепленности продового кода, негативные эффекты мейнстримного подхода ещё не успели бы проявиться. А вот в тестах - уже бы проявились в полный рост. В итоге, я полагаю, трудозатраты на реализацию бы выросли на 10-20% (на актуализацию моков), а трудозатраты на исправление багов, пропущенных тестами на моках, выросли бы до 20-25%.
- Гипотетический мейнстримный бэк
- Общие трудозатраты: 562.2-585.7 часа
- Реализация: 468.6 (426 часов факта ЭП-версии + 10%)
- Исправление багов: 93.7-117.1 (20-25% от 468.6)
- Количество задач: - (не знаю, как хоть сколько-нибудь адекватно оценить и выровнять с общими трудозатратами)
- Количество багов: - (не знаю, как хоть сколько-нибудь адекватно оценить и выровнять с общими трудозатратами)
- Медианные трудозатраты на задачу: 5.5-6 (5 + 10-20%)
- Общие трудозатраты: 562.2-585.7 часа
Реализация оригинальной версии и её реинжиниринг
При выполнении реинжиниринга, за счёт использования Hibernate трудозатраты на реализацию сократились бы процентов на 20 и ещё процентов на 10 за счёт тестов на моках. С другой стороны, трудозатраты на исправление багов удвоились бы за счёт багов, пропущенных тестами на моках. Наконец, административные трудозатраты не изменились бы.
- Гипотетический мейнстримный бэк
- Общие трудозатраты: 965.4
- Реализация: 596.4 (70% от 852 часов факта ЭП-версии)
- Исправление багов: 118 (59 часов факта ЭП-версии + 100%)
- Административные задачи: 251
- Общие трудозатраты: 965.4
Итого
Итого общие трудозатраты на "первые две версии" (реинжиниринг и 3 месяца саппорта) по ЭП составили 2039 часов. А оценочные общие трудозатраты на "первые две версии" по мейнстримному подходу составили бы 1814.5-1849.
То есть первый год разработки по ЭП будет примерно на 10% дороже.
Однако, как показывает моя практика, при разработке по мейнстримному подходу, трудо- и баго-ёмкость задач растёт очень быстро.
В случае же ЭП, предположительно, они будут расти намного медленнее.
Это я и собираюсь проверить - я надеюсь, Проект Э проживёт ещё хотя бы пару лет (все предпосылки к этому есть) и я смогу ещё хотя бы три-четыре раза с интервалом в 3-6 месяцев повторить это упражнение и оценить тренд роста трудозатрат и количества багов на задачу при работе с эргономичной кодовой базой.
Выводы
Итак. Стоило ли делать реинжиниринг? Безусловно да, на основе данных из Jira можно с уверенностью утверждать, что мы смогли снизить трудозатраты и количество багов как минимум в два раза. Это улучшение ещё "усугубляется" за счёт того, что для заказчика внешние рейты штатных kotlin-истов ниже внешних рейтов .net-чиков аутстафферов.
Стоило ли делать реинжинирнг по Эргономичному подходу? Доподлино неизвестно. Гипотетически, при условии, что работы продолжатся ещё хотя бы год, и если я прав, что показатели будут деградировать очень медленно, - да. Но это всё теория.
Кроме того, результаты анализа данных дают дополнительное подтверждение общеизвестным утверждениям:
- Первый год разработки на микросервисах дороже разработки на монолите. Минимум на 30%;
- Автоматизация тестирования снижает количество багов и трудозатрат на их устранение. Минимум в два раза;
- Мотивация команды имеет огромное влияние на трудозатарты. От 30% дополнительных трудозатрат в случае низкой мотивации.