Одной из основных причин разработки микросервисов является то, что они обеспечивают четкие границы модулей.
Однако минусы микросервисов настолько велики, что это все равно, что отрубить себе правую руку, чтобы научиться писать левой. Есть более управляемые (и менее болезненные!) способы достижения того же результата.
Даже с тех пор, как началось повальное увлечение микросервисами, возобладали некоторые более хладнокровные. В частности, Оливер Дротбом, разработчик Spring framework, долгое время был сторонником альтернативы moduliths. Идея заключается в том, чтобы сохранить монолит, но спроектировать его на основе модулей.
Многие выбирают микросервисы, потому что приложение, над которым они работают, напоминает блюдо со спагетти. Если бы их приложение было лучше спроектировано, тяга к микросервисам не была бы такой сильной.
Зачем нужна модульность?
Модульность — это способ уменьшить влияние изменений на кодовую базу. Это очень похоже на то, как проектируют большие корабли.
Когда вода постоянно просачивается в корабль, последний обычно тонет из-за уменьшения силы Архимеда. Чтобы избежать затопления корабля из-за единственной утечки, его проектируют с несколькими водонепроницаемыми отсеками.
Если произойдет одна утечка, она локализуется в одном отсеке. Хотя это и не идеальный вариант, но он предотвращает затопление корабля, позволяя ему перенаправиться в ближайший порт, где его можно отремонтировать.
Модульность работает аналогично: она устанавливает границы вокруг частей кода. Таким образом, эффект от изменения ограничивается этой частью и не распространяется за ее границы.
В Java такие части известны как пакеты. Параллель с кораблями на этом заканчивается, потому что пакеты должны работать вместе для достижения желаемых результатов. Пакеты не могут быть «водонепроницаемыми». Язык Java предоставляет модификаторы видимости для работы через границы пакетов. Интересно, что самый известный из них public
позволяет полностью пересекать границы пакетов.
Разработка границ, соответствующих принципу наименьших привилегий, — это постоянная работа. Велика вероятность того, под давлением проекта на начальном этапе разработки или со временем во время технического сопровождения усилия будут ослабевать, и границы будут разрушаться.
Нам нужен более совершенный способ обеспечения соблюдения границ.
Модули, модули повсюду
На протяжении долгой истории Java «модули» были решением для обеспечения соблюдения границ. Дело в том, что даже сегодня существует множество определений того, что такое модуль.
Проект разработки технологии OSGI, запущенный в 2000 году, был нацелен на предоставление версий компонентов, которые можно было бы безопасно развертывать и удалять во время выполнения. Она сохранила единицу развертывания JAR, но добавила метаданные в свой манифест. OSGi была мощной, но разработка OSGi bundle (название модуля) была сложной. Разработчики платили более высокую стоимость за разработку, в то время как команда эксплуатации пользовалась преимуществами при развёртывании. DevOps еще только предстояло родиться, он не сделал OSGi таким популярным, каким он мог бы быть.
Параллельно с этим архитекторы Java искали свой путь к модуляризации JDK. Этот подход намного проще по сравнению с OSGI, поскольку он позволяет избежать проблем с развертыванием и управлением версиями. Модули Java, представленные в Java 9, ограничиваются следующими данными: именем, публичным API и зависимостями от других модулей.
Модули Java хорошо работают для JDK, но гораздо хуже для приложений из-за проблемы «курицы и яйца». Чтобы быть полезными для приложений, разработчики должны разбивать библиотеки на модули, не полагаясь на автоматические модули. Но разработчики библиотек будут делать это только в том случае, если достаточное количество разработчиков приложений будут этим пользоваться. В последний раз, когда я проверял, только половина из 20 общих библиотек были модульными.
Что касается сборки, я должен упомянуть модули Maven. Они позволяют разделить код на несколько проектов.
Существуют и другие системы модулей для JVM, но эти три наиболее известны.
Ориентировочный подход к обеспечению соблюдения границ
Как упоминалось выше, микросервисы обеспечивают окончательную границу во время разработки и развертывания. В большинстве случаев это чрезмерно. С другой стороны, нельзя отрицать, что проекты со временем загнивают. Даже самый красиво сделанный проект, в котором дорожили модульностью, без постоянного ухода неизбежно становиться беспорядочным.
Нам нужны правила для обеспечения соблюдения границ, и к ним нужно относиться как к тестам: когда тесты не работают, код нужно исправлять.
Точно так же, когда человек нарушает правило, он должен это исправить. ArchUnit — это инструмент для создания и применения правил. Он позволяет настраивать правила и проверять их как тесты. К сожалению, настройка занимает много времени и должна постоянно поддерживаться, чтобы обеспечить ценность. Вот фрагмент примера приложения, следующего принципу гексагональной архитектуры:
HexagonalArchitecture.boundedContext("io.reflectoring.buckpal.account")
.withDomainLayer("domain")
.withAdaptersLayer("adapter")
.incoming("in.web")
.outgoing("out.persistence")
.and()
.withApplicationLayer("application")
.services("service")
.incomingPorts("port.in")
.outgoingPorts("port.out")
.and()
.withConfiguration("configuration")
.check(new ClassFileImporter()
.importPackages("io.reflectoring.buckpal.."));
Обратите внимание, что класс HexagonalArchitecture
представляет собой пользовательский DSL-фасад поверх ArchUnit API.
В целом, ArchUnit лучше, чем ничего, но лишь незначительно. Его основным преимуществом является автоматизация с помощью тестов. Он значительно улучшился бы, если бы архитектурные правила можно было выводить автоматически. Именно эта идея лежит в основе проекта Spring Modulith.
Spring Modulith
Spring Modulith является преемником проекта Moduliths Оливера Дротбома (с буквой S в конце). Он использует как ArchUnit, так и jMolecules. На момент написания этой статьи он был экспериментальным.
Spring Modulith позволяет:
Документировать отношения между пакетами проекта.
Ограничивать определенные отношения.
Проверять ограничения во время тестирования.
Он требует, чтобы приложение использовало Spring Framework: он использует способность их взаимопонимания, получаемую при сборке с помощью DI (инверсии зависимостей).
По умолчанию модуль Modulith представляет собой пакет, расположенный на том же уровне, что и класс с аннотацией SpringBootApplication
.
![](https://habrastorage.org/getpro/habr/upload_files/ad8/a65/72f/ad8a6572f46b9361a32a080b5e81c4d0.png)
Класс приложения
Modulith модуль
Не модуль
По умолчанию модуль может получить доступ к содержимому любого другого модуля, но не может получить доступ к подпакетам других модулей.
Spring Modulith предоставляет сервис формирования диаграмм на основе текста с помощью PlantUML со скинами UML или C4 (по умолчанию). Генерация диаграмм проста как пирог:
var modules = ApplicationModules.of(DummyApplication.class);
new Documenter(modules).writeModulesAsPlantUml();
Чтобы прервать сборку, если модуль обращается к обычному пакету, вызовите в тесте метод verify()
.
var modules = ApplicationModules.of(DummyApplication.class).verify();
Пример для экспериментов
Я создал пример приложения для экспериментов: оно эмулирует главную страницу интернет-магазина. Главная страница генерируется на стороне сервера с помощью Thymeleaf и отображает элементы каталога и ленту новостей. Последняя также доступна через HTTP API для вызовов на стороне клиента (кодировать который мне было лень). Товары отображаются с указанием цены, поэтому требуется служба ценообразования.
Каждая функция — страница, каталог, лента новостей и ценообразование — находится в пакете, который рассматривается как модуль Spring. Функция документирования Spring Modulith генерирует следующее:
![](https://habrastorage.org/getpro/habr/upload_files/9d6/f3b/21a/9d6f3b21a8a0b59f7af5de84ed048164.png)
Давайте проверим дизайн функции ценообразования:
![](https://habrastorage.org/getpro/habr/upload_files/9a6/6b3/7ed/9a66b37edadccdd724c7b7a4ebd6b62e.png)
В текущем дизайн имеются две проблемы:
PricingRepository
доступен за пределами модуляPricingService
раскрывает сущностьPricing
JPA
Мы исправим дизайн, путем инкапсуляции типов, которые не должны быть открыты. Мы перенесем типы Pricing
и PricingRepositoryinternalpricing
во внутреннюю подпапку модуля ценообразования:
![](https://habrastorage.org/getpro/habr/upload_files/fa6/935/b72/fa6935b72350cfd8c32b0464f72d6787.png)
Module 'home' depends on non-exposed type ch.frankel.blog.pricing.internal.Pricing within module 'pricing'!
Устраним нарушения следующими изменениями:
![](https://habrastorage.org/getpro/habr/upload_files/307/084/66f/30708466f1b976ddb5a53f3a582e9e8f.png)
Заключение
Поработав с примером приложения, я оценил Spring Modulith.
Я вижу два основных сценария использования: документирование существующего приложения и сохранение «чистоты» дизайна. Последнее позволяет избежать эффекта «загнивания» приложений с течением времени. Таким образом, мы можем сохранить дизайн таким, каким он был задуман, и избежать эффекта «спагетти».
Вишенка на торте: это замечательно, когда нам нужно нарезать одну или несколько функций на их единицы развертывания. Это будет очень простой шаг, без потери времени на распутывание зависимостей. Spring Modulith дает огромное преимущество: откладывать каждое важное архитектурное решение до последнего возможного момента.
Благодарю Оливеру Дротбому за отзыв.
Полный исходный код этого поста можно найти на Github.