Что я делаю не так
November 6, 2021
"Алексей - один из лучших разработчиков, с которыми я работал. Работой с ним я удовлетворён на 10 баллов из 10, потому что он практически всегда попадает в озвученные сроки, и в работе с ним не приходится накидывать 50% на всякий пожарный".
Привет! Алексей - это я. В этом посте я расскажу, что я делаю не так, как принято, чтобы получать такие отзывы.
Свой подход к разработке я называю Эргономичным и от общепринятого он отличается следующим:
- Я не пишу на Java, а пишу на Kotlin
- Я не использую Spring Data JPA, а использую Spring Data JDBC/R2DBC
- Я не игнорирую концепции состояния системы и эффектов операций, а тщательно их проектирую
- Я не структурирую программы исходя из собственного чувства прекрасного, а применяю принципы структурного дизайна
- Я не использую императивный стиль программирования, а предпочитаю декларативный стиль
- Я не декомпозирую систему на слои, а разделяю на концептуальные модули (пакеты)
- Я не отдаю конфигурацию системы на откуп Spring-овой автомагии, а описываю её руками
- Я не пишу тесты, которые проверяют детали реализации, а пишу тесты, которые проверяют поведение
- Я не использую микросервисную архитектуру для структурирования системы, а начинаю с модульных монолитов
Давайте разбираться, почему я всё это делаю не так, как принято и к чему это приводит для бизнеса.
Язык программирования
Как это ни странно, Java-команды для разработки обычно выбирают язык Java. Действительно, Java 17 - неплохой язык программирования, а эргономичный подход можно использовать практически с любым языком. Но некоторые языки делают разработку дешевле, а применение эргономичного подхода проще, чем другие.
Для себя я в качестве основного языка разработки выбрал Kotlin ещё в 2015 году.
Kotlin обладает рядом особенностей, которые положительно влияют и на качество продукта, и на стоимость его разработки:
- Поддержка nullable-значений на уровне системы типов. По данным Google [fc], в Google Play количество NullPointerException-ов в приложениях на Kotlin на 33% меньше, чем в приложениях на Java. Я думаю одна эта особенность языка на 1-2% снижает стоимость разработки и на столько же увеличивает лояльность пользователей.
- Корутины. В Java нет и не предвидится прямого аналога корутин. Базовая для Java модель конкурентного программирования - на потоках - и масштабируется для IO-интенсивных нагрузок намного хуже, чем корутины и намного более подвержена ошибкам [sc]. Библиотеки, реализующие акторный подход, обладают схожими характеристиками по масштабируемости и надёжности, но намного более громоздки в работе, чем корутины. Project Loom, не выпущен, и не даёт защиты от утечек асинхронных задач. Project Reactor, даёт схожую степень масштабируемости, но не даёт тот же уровень надёжности. Точных данных у меня нет, но я думаю корутины также дают 1-2% снижения стоимости разработки и увеличения стабильности работы приложения.
- Kotlin Multiplatform. У меня есть опыт успешного выделения самой сложной и дорогой части Android-приложения в общий модуль, с последующим его переиспользованием в iOS-приложении. Подобного коммерческого опыта с выделением общего кода между сервером и клиентами у меня нет, но я вижу у этой технологии потенциал для сокращения стоимости разработки систем на десятки процентов.
- По данным JetBrains программы на Kotlin содержат на 40% меньше строк исходного кода, чем аналогичные программы на Java [kf]. Это на 40% меньше усилий на написание кода, чтение кода, ревью кода и на экспоненциально меньше ошибок [cc]. Не думаю, что это даёт 40% сокращения стоимости разработки, но процентов 5-10 должно дать.
- Есть ещё целый ряд небольших особенностей - функции расширения, более богатая стандартная библиотека, инлайн функции, овеществлённые дженерики и т.д. - которые по моей субъективной оценке дают ещё 1-2% сокращения стоимости разработки.
Наконец, в контексте эргономичного подхода, лучшая поддержка декларативного программирования (предпочтение неизменяемых переменных и структур данных, функциональные типы и т.п.) Kotlin даёт ещё 1-2% процента снижения стоимости разработки.
Суммарно всё это даёт удешевление разработки на 10-15%.
Технология работы с БД
Обычно для работы с БД Java-команды без какого-либо критического анализа выбирают Spring Data JPA.
Одним этим решением команда обрекает бизнес на дорогой в разработке продукт с большим количеством ошибок и регрессий и постоянным ростом стоимости и сроков реализации требований. Технология JPA - крайне неудачна и её драматическое влияние на качество всего продукта в целом я рассмотрю ниже. Если же говорить только о технической стороне, то выбор JPA ведёт к:
- Низкой отзывчивости приложения, из-за:
- Чрезмерной сложности технологии и, как следствие, большой разницей между поведением программы в голове программиста и реальным поведением [jpas]
- Лаконичности и "красоты" медленного кода (повсеместно использующего ленивую загрузку)
- Кажущейся тяжеловесности быстрого кода (выполняющего необходимый минимум оптимальных запросов, извлекающих из БД только нужные данные)
- Высокой стоимости оборудования, необходимого для обеспечения хоть сколько-нибудь приемлемого уровня производительности
- Большому количеству ошибок из-за:
- Всё той же разницей между поведением программы в голове программиста и реальным поведением
- Возможности неявно и случайно (через мутацию объекта, доступного через несколько ссылок внутри транзакции) внести лишние изменения в хранимые данные
- Большому количеству регрессий. JPA подталкивает программистов к тому, чтобы создавать высокую степень связанности (coupling) между различными модулями, через полносвязный двунаправленный граф всех сущностей системы [jag]. Как следствие, повышается вероятность регрессий в одних модулях, при внесении изменений в другие модули.
- Непредсказуемому времени исправления ошибок. Из-за высокой сложности ("автомагичности") JPA, зачастую программисты сталкиваются с проблемами, которые проявляются только в промышленной эксплуатации. Такие проблемы совершенно непонятно чем вызваны, в какой момент что-то пошло не так и как всё это отлаживать и исправлять. Как следствие, в проектах с JPA зачастую бывают "нормальные" исключения в логах - какие-то ошибки, с которыми не понятно что делать, но в целом можно жить.
Подробнее о проблемах свойственных JPA можно почитать в моём посте, посвящённом этому вопросу.
Для решения описанных проблем я изучил рынок существующих решений для работы с БД, опробовал наиболее перспективные из них на практике и на данный момент остановился на Spring Data JDBC и R2DBC. У этих технологий два ключевых преимущества перед технологиями, основывающимся на JPA:
- В них минимум "автомагии" и из кода работы с БД очевидно какие запросы будут действительно выполнены.
Это положительно сказывается практически на всех аспектах приложения:
- Скорости работы приложения
- Требованиях к ресурсам
- Количеству ошибок
- Времени исправления ошибок
- Использование Spring Data JDBC/R2DBC подталкивает программистов к разделению состояния системы на изолированные агрегаты. Это уменьшает связанность (coupling) системы и как следствие удешевляет её развитие и уменьшает количество регрессий.
В итоге использование Spring Data JDBC/R2DBC означает для бизнеса, что он быстрее и дешевле получит более стабильный и отзывчивый продукт, темп развития которого практически не будет падать со временем.
Управление состоянием и эффектами
Обычно для разработчиков термин "Состояние" ассоциируется только с одноимённым шаблоном проектирования, а "Эффект" вообще не имеет ассоциаций в контексте разработки информационных систем.
Между тем хранение и обработка состояния, является главной целью информационной системы. Конечный пользователь воспринимает систему через эффекты отображения её текущего состояния, а корректность работы системы определяет по набору эффектов, осуществлённых в ответ на его действия. Подробнее о состоянии, эффектах и их значимости можно почитать в моём посте, посвящённом этому вопросу.
Непонимание этих фундаментальных понятий приводит к тому, что команды формируют структуру состояния информационной системы хаотично, без какой-либо стратегии. Это порождает запутанный граф состояния системы, зачастую с циклами и нестабильными зависимостями. Такой граф сложно и понять, и модифицировать.
А пренебрежение эффектами ведёт к тому, что они разбросаны по коду в произвольных местах. Это усложняет понимание того, к каким эффектам приведёт та или иная операция и наоборот, какие операции могут привести к тому или иному эффекту. Также это существенно усложняет автоматизацию тестирования - тестирование кода с эффектами требует подготовки окружающей среды, с последующей проверкой изменений в ней.
Из-за этого разработчикам сложно понять все последствия вносимых изменений, и у них нет надёжной сетки безопасности в виде автоматических тестов. В итоге, особенно под давлением, разработчики вносят вслепую изменения, которые решают поставленную задачу. А выявление побочных эффектов этих изменений отдают на откуп QA и пользователям. Порождая при этом дополнительные циклы разработчик-QA - дополнительные рабочие и календарные часы.
Отдельный вклад в хаос вносит применение JPA, которая:
В итоге для бизнеса всё это выливается в повышенную стоимость и сроки реализации продукта, в том числе из-за большего количества регрессий.
Я же осознанно и целенаправлено формирую структуру состояния системы таким образом, чтобы минимизировать стоимость внесения изменений в неё. Руководствуюсь я при этом давно известными принципами дизайна:
- В структуре состояния системы не должно быть циклов [asdppp]
- Зависимости должны быть направлены в сторону более стабильных элементов [asdppp]
- Детали устройства элементов состояния с высокой вероятностью изменений должны быть инкапсулированы в границах соответствующих модулей [ih]
- Наращивание функциональности системы должно выполняться за счёт расширения структуры состояния, а не модификации существующих элементов [asdppp]
Для того чтобы сделать максимально явной связь операций системы и их эффектов на состояние, я использую менее известные, но тоже проверенные техники:
Применение этих техник даёт:
- Сокращение трудоёмкости реализации требований за счёт того, что:
- Изменения в структуре состояний инкапсулируются в одном модуле, либо затрагивают небольшое количество модулей
- Последствия изменений в эффектах операции инкапсулируются в одном модуле, либо затрагивают небольшое количество модулей, список которых можно получить автоматически
- Минимизацию количества ошибок за счёт упрощения автоматизации тестирования бизнес-логики системы
- Минимизацию количества регрессий, за счёт:
- Минимизации трудоёмкости изменений
- Повышения видимости последствий изменений
- Повышения покрытия автоматическими тестами
То есть мы снова приходим к тому, что бизнес получает более качественный продукт за меньшие деньги.
Стиль программирования
Я не сталкивался с информационными системами, спроектированными другими людьми и не использующие JPA, поэтому не могу сказать является ли связь между JPA и процедурным стилем программирования причинно-следственной или корреляционной. Тем не менее JPA не оставляет выбора и вынуждает использовать устаревший процедурный стиль кодирования, когда пакеты процедур (Сервисы) императивно модифицируют структуры данных (Сущности) [auap].
JPA настолько сложна и непредсказуема, что программисты боятся применять принципы объектно-ориентированного программирования и помещать какой-либо код кроме сеттеров и геттеров в сущности. Кроме того, спецификация JPA накладывает ряд ограничений на классы сущностей (обязательная открытость для наследования, обязательный конструктор по умолчанию, наличие геттеров и сеттеров), которые непосредственно противоречат принципам объектно-ориентированного дизайна. Подробнее можно почитать в моём посте, посвящённом JPA.
Также JPA исключает и применение функционального подхода, т.к. требует чтобы изменения в состояние вносились посредством императивной модификации изменяемых сущностей.
Остаётся только процедурное программирование. Притом, как правило, хаотичное процедурное программирование, руководимое интерпретациями программистов и ревьюверов нескольких популярных книг вроде Чистого Кода Роберта Мартина. Вместо рациональных методик из классических книг [sp], [sd].
Всё это ведёт к разработке сложной системы, с высокой связанностью (coupling) и низкой связностью (cohesion), что для бизнеса выливается в высокую стоимость разработки и поддержки, большое количество ошибок, долгий онбординг новых разработчиков [sd].
От объектно-ориентированной парадигмы я тоже отказался, т.к. несмотря на заявления её сторонников, по моему опыту она плохо подходит для моделирования нашего мира (уступая в этом акторной модели [pe]), а следование принципам ООД на уровне кода (Информационный Эксперт из GRASP, в частности) ведёт к созданию божественных объектов [auap], [dsdp]. В этом выборе я не одинок - анкл Боб, одна из ключевых фигур в ООП/ООД, примерно с 2010 года [ot] предпочитает программировать на функциональном Clojure [wc] (не отказавшись при этом от объектно-ориентированного подхода в дизайне).
А вот использование адекватных технологий (Spring Data JDBC/R2DBC) позволяет мне работать в рамках декларативной парадигмы. Академическое функциональное программирование с высокоуровневыми абстракциями, понятными только хорошо тренированным математикам, я тоже не считаю подходящим средством для разработки информационных систем. Поэтому из функционального программирования я взял только две ключевые на мой взгляд техники - функции без побочных эффектов (чистые функции) и неизменяемые структуры данных. Это позволяет мне получить все выгоды декларативного программирования, оставляя код доступным для понимания всем программистам.
Декларативный подход подразумевает разделение кода описывающего желаемые эффекты (бизнес-логики) и их осуществляющего (ввода-вывода). Однако операции системы в реальном мире подразумевают одновременно и эффекты (в противном случае, операция просто нагреет воздух и не изменит состояние мира) и бизнес-логику (в противном случае, эффекты оставят мир в текущем состоянии). В отсутствие сложных функциональных техник (вроде интерпретаторов свободных монад), естественным способом объединения бизнес-логики и ввода-вывода являются контролирующие модули из структурного дизайна [sd]. Контролирующие модули являются второй отличительной чертой моего стиля кодирования.
Всё вместе это превращается в уже упомянутый мной подход функциональное ядро/императивная оболочка [fcis]. Такой подход кардинально повышает характеристики разработки и продукта, существенные для бизнеса:
- Выделение бизнес-логики в чистые функции упрощает покрытие их автоматическими тестами, включая продвинутые тесты свойств (property testing) [qc]. Это, в свою очередь, снижает количество регрессий и ошибок, снижает стоимость QA, снижает накладные расходы на дополнительные итерации исправления ошибок, повышает качество пользовательского опыта
- Разделение бизнес-логики, ввода/вывода и модулей управления, повышает переиспользуемость отдельных элементов, снижая стоимость разработки
- Декларативный код более прост для понимания, чем императивный и сокращает время онбординга новых разработчиков [wfpm], [tcem], [ddd], [rstc]
- Декларативный код менее подвержен ошибкам при первичной реализации и последующих модификациях и рефакторинге, давая все те же преимущества [wfpm], [them], [ddd], [rstc]
- Чистые функции проще оптимизировать и людям (их выполнение можно без ограничений распараллеливать и свободно кэшировать результат) и компиляторам [tcem], что существенно упрощает оптимизацию "горячих точек" приложения
Декомпозиция системы на пакеты
Обычно дизайн пакетов сводится к разбиению классов на функциональные категории - сервисы, ДТО, исключения и т.п. Однако нет ни одного авторитетного источника, который бы рекомендовал такой подход и существует множество авторитетных источников, критикующих такой подход [ih], [asdppp], [sd], [auap], [ddd], [ca].
Критика сводится к четырём тезисам:
- Механическое пакетирование по функциональным категориям не отражает архитектуру приложения и структуру предметной области и тем самым затрудняет понимание системы [ddd], [iddd], [ca]. Из своего опыта я могу сказать, что есть 100% корреляция между таким пакетированием и архитектурой "Большой Ком Грязи" [bbom], т.е. фактическим отсутствием архитектуры
- Такое пакетирование исключает возможности для сокрытия информации и инкапсуляции [ih], [asdppp], [sd], [auap]. Если говорить о Java, то в этом языке самым удобным является модификатор доступа package private - который используется, если модификатор доступа не был указан явно. То есть дизайнеры языка планировали, что именно package private будет наиболее распространённым [jgoj]. Пакетирование же по функциональности, вынуждает делать все классы и большинство методов публичными. Тотальная публичность элементов кода лишает разработчиков одного из ключевых средств локализации изменений и ведёт к чрезмерной трудоёмкости реализации изменений в требованиях.
- Декомпозиция по функциональности ведёт к низкой связности (cohesion) и высокой связанности (coupling) пакетов [sd], [asdppp], [ddd]. Ещё в 60ых года Ларри Константин доказал, что такие программы сложны в понимании и поддержке и как следствие подвержены большому количеству ошибок и регрессий.
- Механическое разбиение превращает разработчиков в Эллочку-людоедку со словарным запасом из ~8 слов - контроллер, сервис, репозиторий, сущность, ДТО, исключение, фабрика, перечисление. Тем самым искусственно ограничивая разработчика в инструментарии для корректного моделирования предметной области [ddd], [iddd].
Всё это ведёт к всё тем же проблемам для бизнеса: программы с таким принципом пакетирования стоят дороже, требуют больших усилий для реализации новой функциональности и особенно подвержены ошибкам и регрессиям.
Избежать всех этих проблем помогает разбиение классов по пакетам на основе агрегатов DDD и юз кейсов [ddd], [ca], [auap], [oose], с учётом основных принципов проектирования:
- Принцип ацикличного графа зависимостей [adpppp]
- Принцип сокрытия информации [ih]
- Принцип стабильных зависимостей [adpppp]
- Принципы высокой связности (cohesion) и низкой связанности (coupling) [sd], [auap], [ddd]
- Принцип единственности ответственности [adpppp]
- Принцип расширения поведения, за счёт нового кода
Всё это позволяет декомпозировать систему на набор изолированных пакетов, со стабильным ядром и минимальным количеством связей между ними. Кроме того, такой подход позволяет использовать Dependency Structure Matrix [dsm] не только для контроля зависимостей между отдельными классами, но и для контроля зависимостей между модулями.
Для бизнеса это значит, что изменения в требованиях будут требовать минимум усилий и порождать минимум регрессий, а добавление новой функциональности будет выполняться преимущественно за счёт нового кода (и состояния) и тем самым исключать регрессии в принципе. Это ведёт к сокращению стоимости и сроков разработки, в том числе за счёт сокращения количества ошибок и регрессий.
Использование Spring для связки бинов
Обычно разработчики для связки бинов приложения используют Spring Component Scan и, хоть и реже в последнее время, автоматическое связывание на полях.
Моя практика показывает, что такой подход ведёт к полной утере разработчиками контроля за зависимостями в приложении, что ведёт к моментальному появлению циклических зависимостей, зависимостей стабильных классов от нестабильных классов и классов с огромным (более 20) количеством зависимостей. В итоге система быстро скатывается к архитектурному стилю Big Ball of Mud [bbom].
Что влечёт для бизнеса все те же проблемы - постоянно увеличивающиеся стоимость и сроки разработки и количество ошибок и регрессий.
Для того чтобы обеспечить себя и команду инструментарием для контроля за зависимостями и вытащить зависимости на первый план, я применяю рад нестандартных техник конфигурирования Spring-приложений:
- Я отключаю Spring Component Scan
- Я использую инъекцию зависимостей только через конструкторы классов
- Я слежу за тем, чтобы у бинов было не более 3 зависимостей на бины из других пакетов (модулей)
- Я создаю по отдельной Spring Configuration для каждого верхнеуровневого пакета
- В конфигурациях я делаю публичными только бины, определяющие публичный интерфейс пакета, а бины отвечающие за детали его реализации скрываю
- Если какой-то пакет зависит от бина другого пакета, я импортирую не сам бин, а конфигурацию соответствующего пакета
- Каждый класс конфигурации явным образом импортирует все другие конфигурации (включая автоматические), от которых он зависит
Всё это вместе даёт ряд мне инструментов для мониторинга архитектуры и противодействия скатыванию к Big Ball of Mud:
- Архитектура (зависимости между пакетами, в частности) становится доступной в один клин на диаграмме контекстов Spring в IDEA и матрице структуры зависимостей (с гранулярностью на уровне пакетов)
- Добавление новой зависимости начинает требовать существенных усилий (добавление в конструктор класса, изменение класса конфигурации, добавление в конструктор конфигурации) и как следствие создаёт барьер, на котором у программиста появляется шанс остановиться и оценить последствия добавления зависимости
- Появляется несколько дополнительных элементов чек-листа ревью - количество зависимостей в бинах на другие модули, стабильность зависимостей, адекватность области видимости бинов, отсутствие циклов в зависимостях - которые позволяют следить за "здоровьем" архитектуры на регулярной основе
Кроме того, такой подход делает отдельные пакеты полноценными единицами переиспользования с точкой входа в виде Spring Configuration пакета. В частности, это существенно упрощает изолированное тестирование отдельных пакетов программы.
Всё это ведёт к созданию архитектуры с продуманной структурой зависимостей. Хорошо спроектированную структуру легче понять и она минимизирует количество модулей, требующих изменений при изменении в требованиях. Это даёт бизнесу всё те же преимущества - сокращение стоимости и сроков реализации и количества ошибок и регрессий.
Тестирование
Обычно разработчики делают акцент на юнит-тестировании, подразумевая под юнитом отдельный метод. А для изоляции кода от коллабораторов активно используют моки.
Такой подход популяризировала Лондонская школа тестирования [ls], но основатели движения TDD - Кент Бек и Роберт Мартин придерживаются других практик [tdd], [wtm], [tab], [wtddgw], [tddid].
Потому что Лондонскому подходу свойственен ряд существенных недостатков:
- Такие тесты ничего не говорят о работоспособности системы [wmuaw], [ls], [tim]
- Такие тесты проверяют реализацию, а не поведение и как следствие препятствуют рефакторингу [wtddgw], [ls], [tim]. Что ведёт к его удорожанию, как следствие, отказу от него, как следствие - постепенному сгниванию дизайна.
- Такие тесты оторваны от потребностей заказчика [wtddgw], [ls]. Заказчика не интересует, выбрасывает ли метод Foo.bar исключение при определённых значениях входных параметров. Заказчика интересует реализовано ли требование запрета на перевод денег, в случае нехватки средств на исходном счету.
- Ценность, как правило, возникает именно в интеграциях. Уникальный алгоритм торговли на бирже с доходностью 150% совершенно бесполезен, без интеграции с этой биржей. А Лондонская школа тестирования держит интеграции вне своего фокуса [ls], [tim].
Для бизнеса это значит, что такие тесты лишь увеличивают стоимость первоначальной разработки и последующей поддержки, при этом практически никак не влияют на стоимость QA, количество фич возвращённых разработчикам на исправление ошибок и регрессий и количество ошибок и регрессий, дошедших до конечных пользователей.
Поэтому я пишу тесты, которые проверяют, что система удовлетворяет требованиям и мокирую только внешние системы, которые слишком дорого использовать в тестах, или которые работают нестабильно.
Такие тесты позволяют мне:
- Сократить на порядок количество задач возвращённых QA на исправление ошибок и регрессий и количество ошибок и регрессий, дошедших до конечного пользователя
- Выполнять рефакторинг (изменение структуры кода, без изменения поведения) без изменений в тестах
- Быть уверенным в том, что если все тесты прошли, то система удовлетворяет всем согласованным требованиям
Для бизнеса это, как обычно, значит сокращение стоимости и сроков разработки, и меньшее количество ошибок и регрессий дошедших до конечного пользователя.
Подробнее о моём подходе к тестированию и результатам, которые он даёт можно почитать в посте Тесты которым можно доверять.
Архитектура
С точки зрения архитектуры сейчас наиболее распространены три варианта, усложняющие жизнь разработчикам и бизнесу.
Исторически самым распространённым архитектурным стилем был слоёный монолит, который ввиду описанных выше обычаев разработки уже через три-четыре месяца разработки превращался в Layered Big Ball of Mud.
После начала хайпа микросервисов многие команды начали делать системы по микросервисной архитектуре и получается у них сильно по-разному.
Если команда не была в состоянии спроектировать модульный монолит, а переход на микросервисную архитектуру не послужил триггером к качественному скачку в дизайнерской квалификации, то команда получала наихудший из возможных вариантов - Distributed Big Ball of Mud [mm], [mom].
Если команде удалось хорошо спроектировать микросервисы, но декомпозиция системы была единственной причиной их применения, то команда зря заплатила огромные накладные расходы на старте разработке и при последующей поддержке [mm], [mom].
Применение микросервисной архитектуры со старта проекта уместно в двух случаях [mm], [mom]:
- Проект разрабатывается для 1% компаний, которым действительно важны операционные свойства микросервисов - независимые масштабируемость, развёртывание, конфигурируемость и т.п.
- Архитектура разрабатывается для крупной корпорации, которая планирует львиную долю работ отдать на аутсорс и аутстаф разным командам
Наконец, в последнее время набирает хайп Чистая Архитектура Роберта Мартина [ca]. Этому хайпу поддался и я и сделал три коммерческих проекта по Чистой Архитектуре. В результате пришёл к выводу, что она практически ничего не даёт относительно от остальных моих практик, с точки зрения борьбы с Big Ball of Mud, зато несёт с собой существенные накладные расходы, удорожая разработку.
К счастью или несчастью, я никогда не разрабатывал архитектуру для проектов, в которых микросервисы были бы уместны со старта. Поэтому я (снова, после отхода от чистой архитектуры) начинаю проекты с самой простой и дешёвой архитектуры - слоёного монолита. А для того чтобы он не скатился к Big Ball of Mud, я пользуюсь описанными выше практиками и делаю модульный слоёный монолит. В отдельных модулях, где я вижу что применение Чистой Архитектуры окупит свою стоимость - я на уровне модуля применяю чистую архитектуру.
Пока что ни один из моих проектов, выполненных по эргономичному подходу, не дорос до потребностей к операционной работе, которые могут обеспечить только микросервисы, поэтому все они ещё монолитные. Но я держу в голове потенциальную необходимость выделять из монолита микросервисы и слежу за тем, чтобы модули могли быть выделены в самостоятельные приложения в течение нескольких часов.
Такой подход совмещает в себе лучшее двух миров - отсутствие лишних расходов на старте, и возможность быстро выделить функциональность, для которой появились особые операционные требования, в отдельный микросервис [mm], [mom].
Для бизнеса это значит существенное сокращение сроков и стоимости разработки и поддержки продукта.
Что получается, когда делаешь не так
Эргономичный подход - это согласованный набор принципов и техник, которые я по крупицам собирал и проверял на практике последние десять лет. Эти принципы и техники я взял из признанных и проверенных источников от авторов с мировом именем в ИТ - эргономичный подход несёт с собой ноль рисков относительно общепринятого подхода. Все они служат глобальной цели - минимизации усилий, необходимых для реализации требований, в том числе за счёт сокращения количества ошибок и регрессий. Игнорирование практик эргономичного подхода, помимо технических проблем, влечёт ещё и проблемы с командой.
Мотивация разработчиков
Для разработчиков обычный подход к разработке создаёт две проблемы:
- Реализация изменений в требованиях выливается в неадекватно большие усилия
- Реализация изменений в требованиях приводит к большому количеству регрессий
По моему опыту, в таких условиях разработчики сначала прокрастинируют, а потом увольнялся в поисках лучших условий. Тем самым ещё больше увеличивая стоимость и сроки реализации продукта для бизнеса.
Я тоже так делал. Но после третьей смены работы, я понял, что подход к работе и условия везде одинаковые. И мне необходимо самому создать для себя комфортные условия. Вот почему я начал делать эргономичный подход. Вот почему с рождением ребёнка я не забросил эргономичный подход, а начал вставать в 5 утра чтобы урвать час для работы над ним.
И думаю я на верном пути. К текущему моменту я сделал в эргономичном подходе четыре проекта, командами в два-четыре разработчика. В этих проектах у меня ни разу не возникало потребности мотивировать кого-то. Ушёл с проекта только один человек.
Высоко замотивированная команда с низкой ротацией даёт бизнесу всё те же выгоды - сокращение стоимости и сроков реализации продукта.
Разница в деньгах
Я утверждаю - для бизнеса, эргономичный подход выливается в более качественный продукт за меньшие деньги.
У меня нет результатов "рандомизированных двойных слепых плацебо контролируемых" исследований, на выборке статистически достоверного размера, для подтверждения этого тезиса. Однако ряд аргументов в его подкрепление я могу привести:
- Ларри Константин, первооткрыватель морфологии ориентированной на трансформации, одной из основных техник эргономичного подхода, проводил исследования на более чем ста системах. И в результате пришёл к выводу, что соответствие этой морфологии является надёжным классификатором для разделения систем на дешёвые (в случае соответствия) и дорогие
- Субъективно я чувствую себя более эффективным при работе над системами, сделанными по эргономичному подходу. Мне реже приходят ошибки и регрессии от QA, я реже испытываю давление по срокам, я реже раздражаюсь на чрезмерно трудоёмкий рефакторинг
- В проектах, сделанных по эргономичному подходу, я трижды проводил крупные рефакторинги и ни разу не внёс регрессий
- По статистике, ошибки я в среднем допускаю одну за спринт
- По статистике, регрессии я в среднем допускаю одну в квартал
- Руководитель проекта, сделанного мной по последней версии эргономичного подхода, дал мне такую рекомендацию: "Алексей - один из лучших разработчиков, с которыми я работал. Работой с ним я удовлетворён на 10 баллов из 10, потому что он практически всегда попадает в озвученные сроки, и в работе с ним не приходится накидывать 50% на всякий пожарный"
Вот к чему ведёт то, что я делаю не так, как это принято.
Ссылки
- Agile Software Development, Principles, Patterns, and Practices [asdppp]
- Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development [auap]
- Big Ball of Mud [bbom]
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design [ca]
- Code Complete [cc]
- Domain-Driven Design: Tackling Complexity in the Heart of Software [ddd]
- Deconstructing SOLID design principles [dsdp]
- Dependency Structure Matrix for Software Architecture [dsm]
- Fewer crashes and more stability with Kotlin [fc]
- Functional Core, Imperative Shell [fcis]
- Hibernate Best Practices [hbp]
- Implementing Domain-Driven Design [iddd]
- On the Criteria To Be Used in Decomposing Systems into Modules [ih]
- Map Associations with JPA and Hibernate – The Ultimate Guide [jag]
- James Gosling on Java, May 2000 A Conversation with Java’s Creator, James Gosling [jgoj]
- JSR 338: JavaTM Persistence API, Version 2.2 [jpas]
- Kotlin FAQ [kf]
- What is a Unit Test? Part 2: classical vs. London schools [ls]
- Modular Monoliths [mm]
- Modules or Microservices? [mom]
- Object-Oriented Software Engineering: A Use Case Driven Approach [oose]
- On Types [ot]
- Programming Erlang: Software for a Concurrent World [pe]
- QuickCheck: a lightweight tool for random testing of Haskell programs [qc]
- Reliable software through composite design [rstc]
- Structured Concurrency [sc]
- Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design [sd]
- Structured Programming [sp]
- Testing at the boundaries [tab]
- The Curse of the Excluded Middle [tcem]
- Test Driven Development: By Example [tdd]
- Is TDD Dead? [tddid]
- Testing, induction, and mocks [tim]
- Why Clojre? [wc]
- Why functional programming matters [wfpm]
- Why Most Unit Testing is Waste [wmuaw]
- TDD, Where Did It All Go Wrong [wtddgw]
- When to Mock [wtm]