Слово переводчика
Привет, меня зовут Андрей и я разработчик. Наша команда работает над мобильным приложением для стартапа Dodo Brands — сети кофеен Дринкит. Несмотря на популярность микросервисов, при проектировании бэкенда для мобильного приложения мы всё-таки решили не торопиться и развиваться последовательно. Поэтому наш бэкенд — это модульный монолит.
Модульный монолит — это подход к проектированию приложений, который позволяет, с одной стороны, отложить во времени операционную сложность использования микросервисов, а с другой — избежать превращения монолитной системы в большой комок грязи.
Сама идея модульности не нова и основана на давно известных принципах Separation of Concerns и Information Hiding. Но не так-то просто перейти от абстрактных принципов к пониманию, как их реально использовать на практике.
Для меня важным источником знаний стал проект Камиля Гржибека, в который мне посчастливилось контрибьютить. А основные идеи, которые автор вкладывает в понятие модульной архитектуры, он подробно описывает в серии статей своего блога. На Хабре не так уж много информации о модульных монолитах в целом и практически ничего о конкретных вариантах их реализации. Поэтому под катом перевод первой статьи серии.
Много лет прошло с начала расцвета микросервисов, но это до сих пор одна из главных тем, обсуждаемых в контексте архитектуры программных систем. Популярность облачных решений, контейнеризация, новые удобные инструменты для разработки и поддержки распределённых систем (такие как Kubernetes) ещё больше способствуют этому явлению.
Из наблюдений за тем, что происходит в сообществах, компаниях и из обсуждений с разработчиками складывается впечатление, что большинство новых проектов реализуются в микросервисной архитектуре. Более того, распиливание существующих монолитов на микросервисы стало трендом.
Почему я начал с микросервисов статью, посвящённую модульному монолиту? Потому что, на мой взгляд, мы, как IT индустрия, делаем ошибку, применяя микросервисы настолько широко. Вместо того чтобы сфокусироваться на конкретных требованиях к архитектуре, мы часто рассматриваем микросервисы как лекарство от всех болезней. В то же время в этих болезнях по умолчанию обвиняются монолитные приложения. Если вы хоть раз разрабатывали систему, состоящую из более чем одной единицы развёртывания, то знаете, что всё не так безоблачно, как может показаться. Каждая архитектура имеет свои достоинства и недостатки. Каждая решает одни проблемы и в то же время создает другие.
В таком контексте я хотел бы начать серию статей об архитектуре модульного монолита, и делаю это по нескольким причинам.
Во-первых, хочу развеять миф о том, что нельзя построить высококлассную систему в монолитной архитектуре. Во-вторых, хотел бы внести ясность в определение этой архитектуры, потому что многие интерпретируют её по-разному. В-третьих, эти статьи являются дополнением к проекту (reference application), который доступен на GitHub.
Я всегда стараюсь быть точным, когда говорю или пишу о технических или бизнес-вопросах, особенно, когда речь идёт об архитектуре. Уверен, что ясная и целостная картина крайне важна. Поэтому в первой статье я расскажу, что же такое модульный монолит в моём понимании.
Давайте начнём с того, что такое «монолит».
Монолит
Википедия описывает монолитную архитектуру в контексте строительства следующим образом:
монолитная архитектура описывает строения, которые отлиты или высечены из цельного материала, традиционно из камня.
В терминах разработки ПО строения — это программные системы, а материалы — это исполняемый код. Таким образом, монолитная архитектура предполагает ровно одну единицу исполняемого кода и ничего больше.
Давайте теперь обратимся к двум определениям из мира ПО. Первое о монолитной системе:
программная система называется монолитной, если она имеет монолитную архитектуру, в которой разные аспекты функциональности (например, ввод-вывод, обработка данных и ошибок, пользовательский интерфейс) связаны воедино, а не содержатся в архитектурно независимых компонентах.
И второе о монолитной архитектуре:
монолитная архитектура — это традиционная универсальная модель проектирования ПО. Монолитный в данном контексте значит собранный в единое целое. Компоненты программы связаны и взаимозависимы, а не обладают слабой связанностью (low coupling — прим. перев.), как в случае модульных программ.
Определения выше (одни из первых результатов поиска в Google) основаны на двух предположениях.
Первое: в монолитной архитектуре все части системы формируют одну единицу развёртывания. И я с этим согласен.
Второе: в монолитной архитектуре отсутствует модульность. И с этим я совершенно не согласен. Фразы «связаны воедино, а не содержатся в архитектурно независимых компонентах» и «компоненты программы связаны и взаимозависимы, а не обладают слабой связанностью» крайне негативно характеризуют такую архитектуру, предполагая, что в ней все части смешаны в беспорядке. Конечно, это может быть так, но не должно. Это не является отличительной чертой монолита.
Таким образом, монолит — это не что иное, как строго одна единица развёртывания. Ни больше, ни меньше.
Модульность
Теперь перейдём к модульности.
Что означает термин «модульный»?
Состоящий из отдельный частей, которые вместе формируют целое / Выполненный из набора отдельных частей, которые могут быть объединены в более крупный объект.
Модульность:
проектирование или производство чего-либо в виде отдельных частей.
Т.к. это общее определение, оно недостаточно, когда речь идёт о разработке. Давайте рассмотрим более специфичное — о модульном программировании:
Модульное программирование — это способ разработки ПО, который подразумевает организацию программы как совокупности независимых, взаимозаменяемых блоков (модулей), каждый из которых содержит всё необходимое для реализации определённого аспекта функциональности. Интерфейс модуля описывает элементы, которые он предоставляет и которые требует для своей работы. Эти элементы интерфейса доступны другим модулям. Реализация модуля содержит исходный код, который соответствует элементам интерфейса.
Хочу отметить три важных момента в этом определении. Чтобы архитектуру можно было назвать модульной, модули должны:
быть независимы и взаимозаменяемы;
иметь всё необходимое для реализации определённой части бизнес-функционала;
иметь чётко определённый интерфейс.
Давайте разберём подробнее каждый пункт.
Модуль должен быть независимым (автономным) и заменяемым
Конечно, модуль не может быть абсолютно независимым, тогда бы вообще отсутствовали интеграции с другими модулями системы. Идея в том, чтобы, следуя принципам слабой связанности и высокой сосредоточенности (Loose coupling and High cohesion), свести эти зависимости к минимуму.
В левой части схемы мы видим модуль с большим количеством зависимостей и явно не можем назвать его независимым. С другой стороны, на правой диаграмме ситуация отличается — зависимостей у модуля меньше.
Но количественная характеристика не является единственной при определении степени зависимости модуля. Также важна сила этих зависимостей. Иными словами, как часто и какое количество методов других модулей используется.
В первом случае, возможно, мы неверно определили границы модулей и на самом деле должны объединить их в один.
Ещё одной характеристикой зависимости является частота изменений модулей, от которых зависит рассматриваемый модуль. Очевидно, что чем выше их стабильность (изменения происходят редко), тем меньше негативное влияние зависимости на рассматриваемый модуль.
Правила формирования зависимостей с точки зрения изменчивости компонентов описывает Stable Dependency Principle (прим. перев.).
Подводя итог, независимость модуля определяется тремя характеристиками:
Количество зависимостей.
Сила зависимостей.
Стабильность модулей, от которых зависит рассматриваемый.
Отношения модулей должны напоминать отношения деловых партнёров, а не сиамских близнецов(С.Макконнелл, Совершенный код) (прим. перев.).
Модуль должен иметь всё необходимое для реализации определённой части бизнес-функционала
Значение термина «модуль» сильно зависит от контекста. Часто модулем называется логический слой в архитектуре: модуль пользовательского интерфейса, модуль бизнес-логики, модуль доступа к данным. В данном контексте это тоже модули, но разделённые по техническому, а не функциональному признаку. Формирование модулей по техническому признаку позволяет локализовать в одном модуле возможные технические изменения.
Добавление или изменение бизнес-функционала обычно затрагивает все слои, что приводит к необходимости изменений во всех технических модулях-слоях.
Что мы делаем чаще: чисто технические изменения или изменения бизнес-функционала? На мой взгляд — второе. Нам редко приходится менять слой доступа к данным, библиотеку логирования или UI-фреймворк. Именно по этой причине в контексте модульного монолита мы говорим о бизнес-модуле, который предоставляет определённую законченную часть бизнес-функционала системы. Такой архитектурный паттерн известен также как «вертикальные слои» (Vertical slices). И вот их мы объединяем в модуль:
При такой архитектуре (и при условии правильного разделения на модули) изменение или добавление функционала чаще всего затрагивает только один модуль. Значит, он становится автономным и способным предоставить определённый функционал самостоятельно.
Правила объединения классов в компонент с точки зрения причины их изменений описывает Common Closure Principle. А в этом треде Камиль Гржибек и Джимми Богард обсуждают Vertical Slices (прим. перев.)
Модуль должен иметь чётко определённый интерфейс
Последней характеристикой модульности является четко определённый интерфейс. Мы не можем говорить о модульной архитектуре, если наши модули не имеют контрактов.
Контракт — это то, что модуль предоставляет вовне, а значит является крайне важным. Контракт — это точка входа в модуль. Хороший контракт — непротиворечивый и минимальный (предоставляет только то, что необходимо клиенту).
О минимальности интерфейса компонента говорит Common Reuse Principle (прим. перев.).
Мы должны поддерживать контракт стабильным (не ломать клиентов) и скрывать детали реализации (инкапсуляция).
Как видно из схемы, контракт модуля может принимать разные формы. Это может быть что-то вроде фасада для синхронных вызовов (как публичные методы REST сервисов). Но контрактом могут быть и публикуемые события для асинхронного взаимодействия. В любом проявлении то, что мы выставляем вовне модуля, становится его публичным API. Таким образом, инкапсуляция является неотъемлемой чертой модульности.
Итог
Монолит — это программная система, состоящая из ровно одной единицы развёртывания.
Монолитная система не подразумевает некачественного дизайна и отсутствия модульности. То, что система монолитная, ничего не говорит о её качестве.
Модульный монолит — это способ проектирования монолитной системы с учётом модульности.
«Настоящий» модуль должен быть независимым, самостоятельным и иметь чётко определённый интерфейс.