Слово переводчика

Привет, меня зовут Андрей и я разработчик. Наша команда работает над мобильным приложением для стартапа Dodo Brands — сети кофеен Дринкит. Несмотря на популярность микросервисов, при проектировании бэкенда для мобильного приложения мы всё-таки решили не торопиться и развиваться последовательно. Поэтому наш бэкенд — это модульный монолит. 

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

Сама идея модульности не нова и основана на давно известных принципах Separation of Concerns и Information Hiding. Но не так-то просто перейти от абстрактных принципов к пониманию, как их реально использовать на практике. 

Для меня важным источником знаний стал проект Modular Monolith with DDD (автор Камиль Гржибек), в который мне посчастливилось контрибьютить. А основные идеи, которые автор вкладывает в понятие модульной архитектуры, он подробно описывает в серии статей своего блога. На Хабре не так уж много информации о модульных монолитах в целом и практически ничего о конкретных вариантах их реализации. Поэтому под катом перевод первой статьи серии.


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

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

Почему я начал с микросервисов статью, посвящённую модульному монолиту? Потому что, на мой взгляд, мы, как IT индустрия, делаем ошибку, применяя микросервисы настолько широко. Вместо того чтобы сфокусироваться на конкретных требованиях к архитектуре, мы часто рассматриваем микросервисы как лекарство от всех болезней. В то же время в этих болезнях по умолчанию обвиняются монолитные приложения. Если вы хоть раз разрабатывали систему, состоящую из более чем одной единицы развёртывания, то знаете, что всё не так безоблачно, как может показаться. Каждая архитектура имеет свои достоинства и недостатки. Каждая решает одни проблемы и в то же время создает другие.

В таком контексте я хотел бы начать серию статей об архитектуре модульного монолита, и делаю это по нескольким причинам.

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

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

Давайте начнём с того, что такое «монолит».

Монолит

Википедия описывает монолитную архитектуру в контексте строительства следующим образом:

монолитная архитектура описывает строения, которые отлиты или высечены из цельного материала, традиционно из камня.

В терминах разработки ПО строения — это программные системы, а материалы — это исполняемый код. Таким образом, монолитная архитектура предполагает ровно одну единицу исполняемого кода и ничего больше.

Давайте теперь обратимся к двум определениям из мира ПО. Первое о монолитной системе:

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

И второе о монолитной архитектуре:

монолитная архитектура — это традиционная универсальная модель проектирования ПО. Монолитный в данном контексте значит собранный в единое целое. Компоненты программы связаны и взаимозависимы, а не обладают слабой связанностью (low coupling — прим. перев.), как в случае модульных программ.

Определения выше (одни из первых результатов поиска в Google) основаны на двух предположениях.

Первое: в монолитной архитектуре все части системы формируют одну единицу развёртывания. И я с этим согласен.

Второе: в монолитной архитектуре отсутствует модульность. И с этим я совершенно не согласен. Фразы «связаны воедино, а не содержатся в архитектурно независимых компонентах» и «компоненты программы связаны и взаимозависимы, а не обладают слабой связанностью» крайне негативно характеризуют такую архитектуру, предполагая, что в ней все части смешаны в беспорядке. Конечно, это может быть так, но не должно. Это не является отличительной чертой монолита.

Таким образом, монолит — это не что иное, как строго одна единица развёртывания. Ни больше, ни меньше.

Модульность

Теперь перейдём к модульности.

Что означает термин «модульный»?

Состоящий из отдельный частей, которые вместе формируют целое / Выполненный из набора отдельных частей, которые могут быть объединены в более крупный объект.

Модульность:

проектирование или производство чего-либо в виде отдельных частей.

Т.к. это общее определение, оно недостаточно, когда речь идёт о разработке. Давайте рассмотрим более специфичное — о модульном программировании:

Модульное программирование — это способ разработки ПО, который подразумевает организацию программы как совокупности независимых, взаимозаменяемых блоков (модулей), каждый из которых содержит всё необходимое для реализации определённого аспекта функциональности. Интерфейс модуля описывает элементы, которые он предоставляет и которые требует для своей работы. Эти элементы интерфейса доступны другим модулям. Реализация модуля содержит исходный код, который соответствует элементам интерфейса.

Хочу отметить три важных момента в этом определении. Чтобы архитектуру можно было назвать модульной, модули должны:

  • быть независимы и взаимозаменяемы;

  • иметь всё необходимое для реализации определённой части бизнес-функционала;

  • иметь чётко определённый интерфейс.

Давайте разберём подробнее каждый пункт.

Модуль должен быть независимым (автономным) и заменяемым

Конечно, модуль не может быть абсолютно независимым, тогда бы вообще отсутствовали интеграции с другими модулями системы. Идея в том, чтобы, следуя принципам слабой связанности и высокой сосредоточенности (Loose coupling and High cohesion), свести эти зависимости к минимуму.

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

Но количественная характеристика не является единственной при определении степени зависимости модуля. Также важна сила этих зависимостей. Иными словами, как часто и какое количество методов других модулей используется.

В первом случае, возможно, мы неверно определили границы модулей и на самом деле должны объединить их в один.

Ещё одной характеристикой зависимости является частота изменений модулей, от которых зависит рассматриваемый модуль. Очевидно, что чем выше их стабильность (изменения происходят редко), тем меньше негативное влияние зависимости на рассматриваемый модуль.

Правила формирования зависимостей с точки зрения изменчивости компонентов описывает Stable Dependency Principle (прим. перев.).

Подводя итог, независимость модуля определяется тремя характеристиками:

  1. Количество зависимостей.

  2. Сила зависимостей.

  3. Стабильность модулей, от которых зависит рассматриваемый.

Отношения модулей должны напоминать отношения деловых партнёров, а не сиамских близнецов(С.Макконнелл, Совершенный код) (прим. перев.).

Модуль должен иметь всё необходимое для реализации определённой части бизнес-функционала

Значение термина «модуль» сильно зависит от контекста. Часто модулем называется логический слой в архитектуре: модуль пользовательского интерфейса, модуль бизнес-логики, модуль доступа к данным. В данном контексте это тоже модули, но разделённые по техническому, а не функциональному признаку. Формирование модулей по техническому признаку позволяет локализовать в одном модуле возможные технические изменения.

Добавление или изменение бизнес-функционала обычно затрагивает все слои, что приводит к необходимости изменений во всех технических модулях-слоях.

Что мы делаем чаще: чисто технические изменения или изменения бизнес-функционала? На мой взгляд — второе. Нам редко приходится менять слой доступа к данным, библиотеку логирования или UI-фреймворк. Именно по этой причине в контексте модульного монолита мы говорим о бизнес-модуле, который предоставляет определённую законченную часть бизнес-функционала системы. Такой архитектурный паттерн известен также как «вертикальные слои» (Vertical slices). И вот их мы объединяем в модуль:

При такой архитектуре (и при условии правильного разделения на модули) изменение или добавление функционала чаще всего затрагивает только один модуль. Значит, он становится автономным и способным предоставить определённый функционал самостоятельно.

Правила объединения классов в компонент с точки зрения причины их изменений описывает Common Closure Principle. А в этом треде Камиль Гржибек и Джимми Богард обсуждают Vertical Slices (прим. перев.)

Модуль должен иметь чётко определённый интерфейс

Последней характеристикой модульности является четко определённый интерфейс. Мы не можем говорить о модульной архитектуре, если наши модули не имеют контрактов.

Контракт — это то, что модуль предоставляет вовне, а значит является крайне важным. Контракт — это точка входа в модуль. Хороший контракт — непротиворечивый и минимальный (предоставляет только то, что необходимо клиенту).

О минимальности интерфейса компонента говорит Common Reuse Principle (прим. перев.).

Мы должны поддерживать контракт стабильным (не ломать клиентов) и скрывать детали реализации (инкапсуляция).

Как видно из схемы, контракт модуля может принимать разные формы. Это может быть что-то вроде фасада для синхронных вызовов (как публичные методы REST сервисов). Но контрактом могут быть и публикуемые события для асинхронного взаимодействия. В любом проявлении то, что мы выставляем вовне модуля, становится его публичным API. Таким образом, инкапсуляция является неотъемлемой чертой модульности.

Итог

  1. Монолит — это программная система, состоящая из ровно одной единицы развёртывания.

  2. Монолитная система не подразумевает некачественного дизайна и отсутствия модульности. То, что система монолитная, ничего не говорит о её качестве.

  3. Модульный монолит — это способ проектирования монолитной системы с учётом модульности.

  4. «Настоящий» модуль должен быть независимым, самостоятельным и иметь чётко определённый интерфейс.

Комментарии (9)


  1. sshikov
    10.02.2022 18:19
    -3

    На Хабре не так уж много информации о модульных монолитах в целом и практически ничего о конкретных вариантах их реализации.

    А зачем? Не, ну в смысле, я согласен, что вам возможно интересно, но по большому счету, вот две крупные рыбины из этого пруда: JavaEE и OSGI. Обе они о том, как строить модульные системы. При этом система работает внутри т.н. «контейнера», который обеспечивает некоторые сервисы для модулей. То есть, в каком-то смысле, построенные на их базе системы и будут такими вот модульными монолитами.

    Почему особо не пишут о них? Возможно, потому что и той и другой уже почти лет по 20? Возможно потому, что широко известные (по рекламе микросервисов) ужастики про монолиты редко целиком соответствуют реальности, так как настоящие монолиты, крупняк, они все модульные — иначе их скорее всего просто не написали бы.


  1. maxzabl
    10.02.2022 18:35

    Монолит — это программная система, состоящая из ровно одной единицы развёртывания.

    "Програмная система" - это совокупность програмного и аппаратного обеспечения для выполнения определенной функции и она никак не может состоят из одной единицы развертывания. Например CRM не может выполнять возложенные на нее функции без БД, сервера приложений, клиента для доступа к этой CRM и т.д.. Но если следовать данной логике, получаем что микросервис сам является "програмной системой", а следовательно и монолитом. А если развивать мысль дальше, то мы придем к тому, что микросервисная архитектура также является одной единцей развервертования и эта единица - информационная система.

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

    Как насчет нормальной обработки отказов в монолитной системе средствами самой системы?

    Модульный монолит — это способ проектирования монолитной системы с учётом модульности.

    «Настоящий» модуль должен быть независимым, самостоятельным и иметь чётко определённый интерфейс.

    Тогда в чем отличие модуля от микросервиса? В том, что работает либо все, либо ничего?


    1. Xneg
      10.02.2022 20:58

      Тогда в чем отличие модуля от микросервиса? В том, что работает либо все, либо ничего?

      Монолит — это программная система, состоящая из ровно одной единицы развёртывания.

      Вроде в вопросе и содержится ответ) микросервис - это отдельная единица деплоя, а модуль - это часть одной большой единицы деплоя.


      1. maxzabl
        10.02.2022 21:38

        docker compose up овер_много_контейнеров.yaml, монолит или много микросервисов?


        1. Xneg
          10.02.2022 21:39

          docker compose, в котором есть redis, postgres и рэббит - это монолит?


          1. maxzabl
            10.02.2022 22:59

            Если рассматривать информационную систему в целом, то постгрес, редис и кроль, а также другие сущности docker compose, например какой-то рест апи (читай микросервис) будут модулями информационной системы. При этом развернуты эти модули могут быть как одна единица (docker compose) так и по отдельности.

            Приведу ещё пример... Дан набор скриптов на PHP, которые выполняют одно простое действие, например на get запрос шлют ответ hello. Этот набор скриптов может интегрироваться в различные cms (wordpress, drupal, bitrix, etc). Одной единицей развертывания с cms он не является, так может устанавливаться в cms сильно позже установки самой сms. Ещё и при этом, этот набор скриптов может работать и отдельно от cms. Этот набор скриптов будет модулем или микросервисом?


    1. AG10 Автор
      11.02.2022 09:24

      Например CRM не может выполнять возложенные на нее функции без БД, сервера приложений, клиента для доступа к этой CRM и т.д.

      Конечный пользователь действительно работает с системой как с единым целым. Но разрабатывать эту систему как единое целое просто невозможно, потому что все ее детали слишком сложно одновременно удержать в голове.

      Об этом говорит Стив Макконел в книге Совершенный код. Он называет управление сложностью основным императивом(законом) разработки. Не погрязнуть в сложности нам помогает декомпозиция. Следуя принципу Separation of concerns нужно разделить одно большое сложное на более маленькие части, которые уже проще осознать и понять. Главное, чтобы эти части получились максимально независимыми(абсолютно независимыми сделать конечно не выйдет) иначе все равно придется "подгружать" в голову дополнительные знания о зависимостях.

      Декомпозируя описанную вами систему, мы можем использовать клиент-серверную архитектуру, которая описывает зоны ответственности и правила взаимодействия каждой составляющей. Далее можно перейти на "сервер"(backend) и декомпозировать его дальше, применяя ту или иную архитектуру(n-tier, sliced, clean). Таким образом в идеале, решая конкретную задачу можно будет сосредоточиться на относительно небольшом участке системы.

      Тогда в чем отличие модуля от микросервиса? В том, что работает либо все, либо ничего?

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

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


    1. AG10 Автор
      11.02.2022 09:30

      А если развивать мысль дальше, то мы придем к тому, что микросервисная архитектура также является одной единцей развервертования и эта единица - информационная система.

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


  1. Xneg
    10.02.2022 23:21

    При этом развернуты эти модули могут быть как одна единица (docker compose) так и по отдельности.

    Ну, это всё же разные единицы деплоя. У них нет общей памяти, они хостятся в разных процессах.

    Отсылаю к книжке https://learning.oreilly.com/library/view/building-evolutionary-architectures/9781491986356/ch04.html#idm45678212630456

    Docker compose в вашем примере - это architectural quanta, архитектурный квант.

    Цитата из книги

    An architectural quantum is an independently deployable
    component with high functional cohesion, which includes all the
    structural elements required for the system to function properly. In a monolithic architecture, the quantum is the entire application

    То есть, есть разница между архитектурным квантом и монолитом.

    Ну и это ответ на второй вопрос

    Этот набор скриптов будет модулем или микросервисом?

    Сервисы не делятся на два множества: монолиты и микросервисы. Есть много разных видов архитектур, включая big ball of mud (отсутствие архитектуры), монолит, модульный монолит, SOA и т.д.

    В зависимости от вашего типа архитектуры набор скриптов может быть и модулем монолита и набором доменных скриптов в SOA и выделенными микросервисами (при условии их отдельного деплоя).