Когда мы создаем программное обеспечение, мы хотим создавать «-ости»: понятность, ремонтопригодность, расширяемость и - в тренде сейчас - декомпозицию (возможность разложить монолит на микросервисы, если возникнет необходимость). Добавьте в этот список свою любимую «способность». Большинство - возможно, даже все - из этих «возможностей» идут рука об руку с чистыми зависимостями между компонентами. Если компонент зависит от всех других компонентов, мы не знаем, какие побочные эффекты будет иметь изменение одного компонента, что затрудняет поддержку кодовой базы и еще более затрудняет ее расширение и декомпозицию. Со временем границы компонентов в кодовой базе имеют тенденцию размываться. Появляются плохие зависимости, что усложняет работу с кодом. Это имеет всевозможные плохие последствия. В частности, разработка замедляется. Это тем более важно, если мы работаем над монолитной кодовой базой, охватывающей множество различных областей бизнеса или «ограниченных контекстов», если использовать жаргон Domain-Driven Design. Как мы можем защитить нашу кодовую базу от нежелательных зависимостей? С тщательным проектированием ограниченных контекстов и постоянным соблюдением границ компонентов. В этой статье показан набор практик, которые помогают в обоих случаях при работе со Spring Boot. Эта статья сопровождается примером рабочего кода на GitHub . Что помогает с соблюдением границ компонентов? Уменьшение видимости. Если мы используем Package-Private видимость для «внутренних» классов, доступ будут иметь только классы в одном пакете. Это затрудняет добавление нежелательных зависимостей извне пакета. Итак, просто поместите все классы компонента в один и тот же пакет и сделайте общедоступными только те классы, которые нам нужны вне компонента. Задача решена? Нет, на мой взгляд. Это не работает, если нам нужны подпакеты в нашем компоненте. Нам пришлось бы сделать классы в подпакетах общедоступными, чтобы их можно было использовать в других подпакетах, открыв их для всего мира. Я не хочу ограничиваться одним пакетом для моего компонента! Возможно, у моего компонента есть подкомпоненты, которые я не хочу показывать снаружи. Или, может быть, я просто хочу отсортировать классы по отдельным сегментам, чтобы упростить навигацию по базе кода. Мне нужны эти дополнительные пакеты! Так что, да, package-private видимость помогает избежать нежелательных зависимостей, но само по себе это, в лучшем случае, неполноценное решение. Что мы можем с этим поделать? Мы не можем полагаться только на package-private видимость. Давайте рассмотрим подход к сохранению нашей кодовой базы чистой от нежелательных зависимостей с использованием интеллектуальной структуры пакета, package-private видимости, где это возможно, и ArchUnit в качестве средства обеспечения там, где мы не можем использовать package-private видимость. Мы обсуждаем подход вместе с примером использования. Допустим, мы создаем компонент биллинга, который выглядит следующим образом: Компонент биллинга предоставляет внешний вид калькулятора счетов. Калькулятор счетов генерирует счет для определенного клиента и определенного периода времени. Чтобы использовать язык Domain-Driven Design (DDD): компонент биллинга реализует ограниченный контекст, который предоставляет варианты использования биллинга. Мы хотим, чтобы этот контекст был как можно более независимым от других ограниченных контекстов. В остальной части статьи мы будем использовать термины «компонент» и «ограниченный контекст» как синонимы. Чтобы калькулятор счетов работал, ему необходимо синхронизировать данные из внешней системы заказов в ежедневном пакетном задании. Это пакетное задание извлекает данные из внешнего источника и помещает их в базу данных. Наш компонент состоит из трех подкомпонентов: калькулятора счетов, пакетного задания и кода базы данных. Все эти компоненты потенциально состоят из пары классов. Калькулятор счетов является public компонентом, а компоненты пакетного задания и базы данных являются внутренними компонентами, которые не должны быть доступны извне компонента биллинга. Давайте посмотрим на структуру пакета, который я предлагаю для нашего биллингового компонента: У каждого компонента и субкомпонента есть internalпакет, содержащий, ну, внутренние классы, и дополнительный apiпакет, содержащий, как вы угадали, классы API, которые предназначены для использования другими компонентами. Такое разделение пакетов между internalи apiдает нам несколько преимуществ: Мы можем легко вкладывать компоненты друг в друга. Легко догадаться, что классы внутри internalпакета нельзя использовать извне. Легко догадаться, что классы внутри internalпакета можно использовать из его подпакетов. Пакеты apiи internalдают нам инструмент для обеспечения соблюдения правил зависимостей с ArchUnit (подробнее об этом позже). Мы можем использовать столько классов или подпакетов в пакете api или internal, сколько захотим, при этом границы наших компонентов по-прежнему четко определены. Если возможно, классы внутри internalпакета должны быть package-private. Но даже если они являются public (и они должны быть public, если мы используем подпакеты), структура пакета определяет четкие и легко отслеживаемые границы. Вместо того, чтобы полагаться на недостаточную поддержку Java для package-private видимости, мы создали архитектурно выразительную структуру пакета, которая может быть легко реализована с помощью инструментов. Теперь давайте посмотрим на эти пакеты. Начнем с databaseподкомпонента: +означает, что класс является public, oозначает, что он является package-private. databaseКомпонент выставляет API с двумя интерфейсами ReadLineItemsи WriteLineItems, которые позволяют читать и записвысать строку заказа клиента из и в базу данных, соответственно. Тип LineItemдомена также является частью API. Внутри databaseподкомпонент имеет класс, BillingDatabaseреализующий два интерфейса: В реализации могут быть несколько вспомогательных классов, но они не имеют отношения к этому обсуждению. Обратите внимание, что это применение принципа инверсии зависимостей. Вместо использования apiпакета, зависящего от internalпакета, использется инверсия зависимость. Это дает нам свободу делать в internalпакете все, что мы хотим, пока мы реализуем интерфейсы в apiпакете. В случае databaseподкомпонента, например, нам все равно, какая технология базы данных используется для запроса к базе данных. Давайте также заглянем в batchjobподкомпонент: Подкомпонент batchjob не предоставляет API доступа к другим компонентам. У него просто есть класс LoadInvoiceDataBatchJob(и, возможно, несколько вспомогательных классов), который ежедневно загружают данные из внешнего источника, преобразуют их и передают их в базу данных биллингового компонента через WriteLineItemsинтерфейс: Обратите внимание, что мы используем @Scheduledаннотацию Spring, чтобы регулярно проверять наличие новых элементов в биллинговой системе. Наконец, содержимое компонента верхнего уровня billing: Компонент billingпредоставляет доступ к интерфейсу InvoiceCalculatorи доменному типу Invoice. Опять же, интерфейс  InvoiceCalculatorреализован внутренним классом, который вызывается BillingServiceв примере. BillingServiceобращается к базе данных через ReadLineItemsAPI базы данных для создания счета-фактуры клиента из нескольких позиций: Теперь, когда у нас есть чистая структура, нам нужна инъекция зависимостей, чтобы связать все это вместе. Чтобы связать все вместе с приложением, мы используем функцию Spring Java Config и добавляем Configurationкласс в internalпакет каждого модуля : Эти конфигурации говорят Spring внести набор компонентов Spring в контекст приложения. Полкомпонент конфигурации databaseвыглядит следующим образом: С помощью аннотации @Configurationмы сообщаем Spring, что это класс конфигурации, который вносит компоненты Spring в контекст приложения. Аннотация @ComponentScanговорит Spring, что нужно включить все классы , которые находятся в том же пакете, что и класс конфигурации (или подпакет) и аннотированные с @Componentкак бины в контекст приложения. Это загрузит наш BillingDatabaseкласс, приведенный выше. Вместо @ComponentScanмы могли бы также использовать @Beanаннотированные фабричные методы внутри @Configurationкласса. Под капотом для подключения к базе данных databaseмодуль использует репозитории Spring Data JPA. Мы включаем их с помощью аннотации @EnableJpaRepositories. Конфигурация batchjobвыглядит так же: Только аннотация @EnableSchedulingдругая. Нам это нужно, чтобы включить аннотацию @Scheduledв нашем bean-компонентеLoadInvoiceDataBatchJob. Наконец, конфигурация компонента верхнего уровня billingвыглядит довольно скучно: С помощью аннотации @ComponentScanэта конфигурация гарантирует, что подкомпоненты @Configurationбудут обнаружены Spring и загружены в контекст приложения вместе с их bean-компонентами. Благодаря этому у нас есть четкое разделение границ не только по измерению пакетов, но и по измерению Spring конфигураций. Это означает, что мы можем настроить таргетинг на каждый компонент и подкомпонент отдельно, обращаясь к его @Configurationклассу. Например, мы можем: Загрузить только один (под) компонент в контекст приложения в рамках интеграционного теста SpringBootTest. Включить или отключить определенные (под) компоненты, добавив аннотацию @Conditional...к конфигурации этого подкомпонента. Заменить компоненты, внесенные в контекст приложения, на (под) компонент, не затрагивая другие (под) компоненты. Однако у нас все еще есть проблема: классы в billing.internal.database.apiпакете являются public, то есть к ним можно получить доступ извне billingкомпонента, что нам не нужно. Давайте решим эту проблему, добавив в игру ArchUnit. ArchUnit - это библиотека, которая позволяет нам проверять утверждения о нашей архитектуре. Они включает проверку того, действительны ли зависимости между определенными классами в соответствии с правилами, которые мы можем сами определить. В нашем случае мы хотим определить правило, согласно которому все классы в internalпакете не используются извне этого пакета. Это правило гарантирует, что классы внутри billing.internal.*.apiпакетов недоступны извне billing.internalпакета. Чтобы управлять нашими internalпакетами при создании правил архитектуры, нам нужно как-то пометить их как «внутренние». Мы могли бы сделать это по имени (то есть рассматривать все пакеты с именем «internal» как внутренние пакеты), но мы также можем отметить пакеты другим именем, для чего создадим аннотацию @InternalPackage: Затем во все наши внутренние пакеты мы добавляем package-info.javaфайл с этой аннотацией: Таким образом, все внутренние пакеты отмечены, и мы можем создать правила для этого. Теперь мы создаем тест, который проверяет, что классы в наших внутренних пакетах не доступны извне: В internalPackages(), мы используем reflection библиотеку для сбора всех пакетов, аннотированных нашей @InternalPackageаннотацией. Затем для каждого из этих пакетов мы вызываем assertPackageIsNotAccessedFromOutside(). Этот метод использует API-интерфейс ArchUnit, подобный DSL, чтобы гарантировать, что «классы, которые находятся вне пакета, не должны зависеть от классов, которые находятся внутри пакета». Этот тест теперь завершится ошибкой, если кто-то добавит нежелательную зависимость к public классу во внутреннем пакете. Но у нас все еще есть одна проблема: что, если мы переименуем базовый пакет (io.reflectoringв данном случае) в процессе рефакторинга? Тогда тест все равно пройдет, потому что он не найдет никаких пакетов в (теперь несуществующем) io.reflectoringпакете. Если у него нет пакетов для проверки, он не может потерпеть неудачу. Итак, нам нужен способ сделать этот тест безопасным при рефакторинге. Чтобы сделать наш тестовый рефакторинг безопасным, мы проверяем наличие пакетов: Новый метод assertPackageExists()использует ArchUnit, чтобы убедиться, что рассматриваемый пакет содержится в классах, которые мы анализируем. Мы делаем эту проверку только для базового пакета. Мы не выполняем эту проверку для внутренних пакетов, потому что знаем, что они существуют. В конце концов, мы идентифицировали эти пакеты по @InternalPackageаннотации внутри internalPackages()метода. Этот тест теперь безопасен для рефакторинга и завершится ошибкой, если мы переименуем пакеты должным образом. В этой статье представлен оригинальный подход к использованию пакетов для модульного построения Java-приложения и сочетается его с Spring Boot как механизмом внедрения зависимостей и с ArchUnit для сбоя тестов, когда кто-то добавил недопустимую межмодульную зависимость. Это позволяет нам разрабатывать компоненты с четкими API и четкими границами, избегая большого шара грязи. Дайте мне знать свои мысли в комментариях! Вы можете найти пример приложения, использующего этот подход, на GitHub . Если вас интересуют другие способы работы с границами компонентов с помощью Spring Boot, вам может быть интересен проект moduliths.