Достигли ли мы эпохи модульной зрелости?
17 ноября 2022 г.Одной из основных причин разработки микросервисов является то, что они обеспечивают жесткие границы модулей. Однако минусы микросервисов настолько огромны, что это все равно, что отрубить себе правую руку, чтобы научиться писать левой; есть более удобные (и менее болезненные!) способы достижения того же результата.
Даже с тех пор, как началось повальное увлечение микросервисами, возобладали некоторые более хладнокровные. В частности, Оливер Дротбом, разработчик среды Spring, долгое время поддерживал модулиты альтернатива. Идея состоит в том, чтобы сохранить монолит, но спроектировать его на основе модулей.
Многие выбирают микросервисы, потому что приложение, над которым они работают, напоминает блюдо со спагетти. Если бы их приложение было лучше спроектировано, привлекательность микросервисов не была бы такой сильной.
Почему модульность?
Модульность — это способ уменьшить влияние изменений на кодовую базу. Это очень похоже на проектирование (больших) кораблей.
Когда вода непрерывно просачивается в корабль, последний обычно тонет из-за уменьшающейся тяги Архимеда. Чтобы корабль не затонул из-за единственной утечки, в нем предусмотрено несколько водонепроницаемых отсеков. Если происходит одна утечка, она содержится в одном отсеке. Хотя это и не идеально, но предотвращает затопление корабля, позволяя ему перенаправиться в ближайший порт, где его можно починить.
Аналогично работает модульность: она ограничивает части кода. Таким образом, эффект изменения ограничивается частью и не распространяется за ее границы.
В Java такие части называются пакетами. Параллель с кораблями на этом заканчивается, потому что пакеты должны работать вместе для достижения желаемых результатов. Упаковки не могут быть «герметичными». Язык Java предоставляет модификаторы видимости для работы за пределами пакетов. Интересно, что самый известный из них, public
, позволяет полностью пересекать пакеты.
Разработка границ, соответствующих принципу наименьших привилегий, требует постоянных усилий. Скорее всего, под давлением проекта на начальном этапе разработки или со временем во время обслуживания усилия будут снижаться, а границы стираться.
Нам нужен более продвинутый способ обеспечения соблюдения границ.
Модули, модули повсюду
На протяжении долгой истории Java «модули» служили решением для обеспечения соблюдения границ. Дело в том, что даже сегодня существует множество определений того, что такое модуль.
OSGI, запущенная в 2000 году, была нацелена на предоставление компонентов с версиями, которые можно было бы безопасно развертывать и удалять во время выполнения. Он сохранил модуль развертывания JAR, но добавил метаданные в свой манифест. OSGi была мощной, но разработка комплекта OSGi (название модуля) была сложной. Разработчики платили больше за разработку, в то время как рабочая группа получала преимущества от развертывания. 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 поверх API ArchUnit.
В целом, ArchUnit лучше, чем ничего, но ненамного. Его основным преимуществом является автоматизация с помощью тестов. Было бы значительно лучше, если бы архитектурные правила можно было вывести автоматически. Это идея проекта Spring Modulith.
Пружинный модуль
Spring Modulith является преемником проекта Moduliths Оливера Дротбома (с буквой S в конце). Он использует как ArchUnit, так и jMolecules. На момент написания этой статьи он был экспериментальным.
Spring Modulith позволяет:
* Документирование отношений между пакетами проекта * Ограничение определенных отношений * Тестирование ограничений во время тестов
Это требует, чтобы ваше приложение использовало Spring Framework: оно использует понимание последней, полученное с помощью сборки DI.
По умолчанию модуль Modulith представляет собой пакет, расположенный на том же уровне, что и класс с аннотациями SpringBootApplication
.
|_ ch.frankel.blog
|_ DummyApplication // 1
|_ packagex // 2
| |_ subpackagex // 3
|_ packagey // 2
|_ packagez // 2
|_ subpackagez // 3
- Класс приложения
- Модулит
- Не модуль
По умолчанию модуль может получить доступ к содержимому любого другого модуля, но не может получить доступ
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 генерирует следующее:
Давайте посмотрим на дизайн функции ценообразования:
Текущий дизайн имеет две проблемы:
* PricingRepository
доступен за пределами модуля.
* PricingService
пропускает объект Pricing
JPA
Мы исправим дизайн, инкапсулировав типы, которые не должны быть раскрыты. Мы перемещаем типы Pricing
и PricingRepository
в подпапку internal
модуля pricing
:
Если мы вызываем метод verify()
, он выбрасывает и ломает сборку, потому что Pricing
недоступен извне модуля pricing
:
Module 'home' depends on non-exposed type ch.frankel.blog.pricing.internal.Pricing within module 'pricing'!
Устраним нарушения следующими изменениями:
Заключение
Поиграв с примером приложения, мне понравился Spring Modulith.
Я вижу два известных варианта использования: документирование существующего приложения и сохранение «чистоты» дизайна. Последнее позволяет избежать эффекта «гниения» приложений с течением времени. Таким образом, мы можем сохранить дизайн таким, каким он был задуман, и избежать эффекта спагетти.
Вишенка на торте: это здорово, когда нам нужно нарезать одну или несколько функций в их единицу развертывания. Это будет очень простой шаг, без потери времени на распутывание зависимостей. Spring Modulith дает огромное преимущество: откладывайте каждое важное архитектурное решение до последнего момента.
Спасибо, Оливер Дротбом за отзыв.
Исходный код можно найти на GitHub.
Первоначально опубликовано на странице A Java Geek 13 ноября 2022 г.
Оригинал