Шестигранная архитектура: руководство по портам и адаптерам для разработчиков

Фото: Ekaterina Korol (Unsplash) — https://unsplash.com/photos/gray-concrete-building-during-daytime-kgsW2ci4ZqE?utm_source=dev48&utm_medium=referral

Источник: Netguru•

Шестигранная архитектура: руководство по портам и адаптерам для разработчиков

Hexagonal architecture keeps business rules in a domain core and pushes every database, API and framework out to replaceable adapters. The pattern isn't about hexagons. It's about where you draw the boundary between business rules and everything else, and how disciplined you stay about keeping…

Гексагональная архитектура держит бизнес‑правила в ядре домена и вынуждает каждый базу данных, API и фреймворк к заменяемым адаптерам. Паттерн не связан с гексагональными фигурами. Он касается того, где вы рисуете границу между бизнес‑правилами и всем остальным, и насколько дисциплинированно вы удерживаете инфраструктуру с другой стороны.

Если вы ошибетесь в определении границы, вы получите слоистую архитектуру с лишней церемонией. Ниже – механика, работающий пример Spring Boot с тестами и когда паттерн стоит своих затрат.

Гексагональная архитектура, также известная как паттерн портов и адаптеров, – способ структурирования приложения так, чтобы ядро домена никогда не зависело от внешних технологий. Ядро определяет интерфейсы, называемые портами. Адаптеры вне ядра реализуют эти порты для конкретной базы данных, брокера сообщений, UI или API.

Алисей Кокберн описал паттерн в 2005 году. Его цель заключалась в том, чтобы приложение могло управляться одинаково пользователями, программами, автоматизированными тестами или пакетными скриптами, и быть developed and tested in isolation from its run-time devices and databases.

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

Почему это называется гексагональной архитектурой?

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

Система с тремя внешними зависимостями всё ещё является гексагональной архитектурой, как и система с девятью. Форма сигнализирует симметрию: входные и выходные интеграции находятся на одном и том же типе границы, а не на «передней» и «задней» части приложения.

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

Аннотированная диаграмма: ядро домена, порты и адаптеры

Представьте ядро домена как гексагон в центре диаграммы, содержащий только простые объекты и бизнес‑правила. Нет импортов фреймворков, нет аннотаций ORM, нет HTTP‑клиента. Вокруг его края находятся порты: интерфейсы, которые ядро определяет, но никогда не реализует.

С левой стороны управляющие адаптеры вводят работу в систему. REST‑контроллер, потребитель сообщений, CLI‑команда и запланированная задача каждый вызывают входной порт, который запускает сервис приложения.

С правой стороны управляемые адаптеры делают противоположное. Ядро вызывает выходной порт, а репозиторий Postgres, клиент S3 или шлюз Stripe реализует его с другой стороны. Ядро никогда напрямую не импортирует ни один из типов адаптеров.

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

Управляющие адаптеры против управляемых адаптеров

Управляющий адаптер вызывает ядро домена; управляемый адаптер вызывается им. Это направление вызова – весь критерий того, какую сторону гексагона вы рассматриваете, и определяет, как вы подключаете внедрение зависимостей в фреймворке, таком как Spring Boot.

На управляющей стороне REST‑контроллер преобразует HTTP‑запрос в объект команды и передаёт его входному порту. Потребитель сообщений делает то же самое для события Kafka, а CLI‑команда – для вызова терминала. Ни один из них не содержит бизнес‑правил. Они адаптируют форму внешнего мира в вызов, который понимает ядро.

На управляемой стороне репозиторий – самый очевидный пример. Ядро определяет выходной порт OrderRepository как интерфейс с методами, такими как findById и save. Адаптер Postgres реализует его с JPA. Адаптер DynamoDB может реализовать тот же интерфейс, и замена одного на другой не меняет ядро и его модульные тесты.

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

Пример использования: размещение заказа

Запрос на размещение заказа попадает на REST‑контроллер, который преобразует тело JSON в PlaceOrderCommand и вызывает входной порт PlaceOrderUseCase. Реализация, PlaceOrderService, создаёт новый агрегат Order, применяет инварианты, такие как положительное количество, и помечает заказ как зарезервированный. Ни в одном из классов не импортируется Spring Boot, JPA или SDK платежей.

Сервис затем вызывает выходной порт OrderRepository, чтобы сохранить агрегат. В продакшене управляемый адаптер реализует этот порт над Postgres. В тестах используется in‑memory реализация, поэтому утверждения выполняются напрямую над состоянием агрегата, без базы данных и HTTP.

Это свойство тестирования в изоляции – то, для чего Кокберн создал паттерн, и на этом строится остальная часть руководства.

Реализация гексагональной архитектуры в Spring Boot

Spring Boot не заставляет использовать гексагональную архитектуру, но внедрение через конструктор и гибкие структуры пакетов делают разделение портов и адаптеров простым для соблюдения. Код домена и приложения не содержит аннотаций Spring: нет @Service, нет @Component. Только адаптеры и конфигурация взаимодействуют с фреймворком.

Если вы разрабатываете на JVM и хотите вторую пару рук для такой структуры, сервисы Java‑разработки Netguru покрывают это.

Типичная структура выглядит так:

Выходной порт – обычный интерфейс, которым владеет слой приложения, а не пакет персистенции:

JpaOrderRepositoryAdapter реализует его, сопоставляет Order и JPA‑сущность, и содержит все аннотации Spring Data. Ни домен, ни пакет приложения не импортируют jakarta.persistence, поэтому принцип инверсии зависимостей сохраняется: ядро определяет контракт, а управляемый адаптер его удовлетворяет.

Подключение происходит в конфигурационном классе, вне ядра:

Spring находит JpaOrderRepositoryAdapter, потому что сам адаптер аннотирован @Repository, и внедряет его там, где требуется OrderRepository. Контейнер собирает граф, но каждая зависимость всё равно указывает внутрь к домену.

Самый распространённый способ, как это ломается в кодовых базах Spring Boot, – @Transactional и JPA‑сущности проникают в сервис приложения, потому что это быстрее в краткосрочной перспективе. Как только сервис импортирует типы персистенции, гексагон становится декоративным, а не структурным. Правило сборки (например, тест ArchUnit, который падает, если пакеты приложения или домена импортируют org.springframework или jakarta.persistence) удерживает границу честной.

Как гексагональная архитектура упрощает тестирование

Ядро домена не зависит от драйвера базы данных, брокера сообщений или HTTP-клиента. Юнит-тесты проверяют бизнес-правила напрямую на обычных объектах: без контекста Spring, без Testcontainers, без сетевых вызовов. Изоляция является структурной, а не вопросом дисциплины.

Для исходящих портов напишите тестовый дубликат вместо того, чтобы запускать реальный экземпляр Postgres. Самостоятельно созданный в памяти репозиторий, который реализует тот же порт, что и производственный адаптер, достаточен для проверки поведения домена:

Этот тест не требует контекста Spring Boot, миграции схемы и Docker-демона. Поскольку доменный модуль не включает Spring или JDBC в свой тестовый classpath, такие наборы тестов остаются быстрыми по мере роста кода.

Сравните это с доменной логикой, которая напрямую вызывает класс JPA-репозитория. Тогда каждый юнит-тест требует фреймворка для мокинга, чтобы имитировать внутренности персистентности, или интеграционного теста с реальной базой данных.

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

Когда следует использовать гексагональную архитектуру?

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

Хороший кандидат — это сервис заказов, который принимает REST-запросы от веб-приложения, потребляет события от складской системы и выполняет nightly-согласование с платежным провайдером. Это три адаптера-привода и как минимум два адаптера-привода, все из которых меняются по разному расписанию вокруг одних и тех же бизнес-правил.

Этот паттерн естественно сочетается с доменным проектированием. Vaughn Vernon's Implementing Domain-Driven Design представляет порты и адаптеры как подходящую архитектуру для ограниченного контекста, и на практике каждый ограниченный контекст обычно заслуживает своего собственного гексагона, а не одного общего ядра, растянутого через контексты. Обертывание портов и адаптеров вокруг кода без реальной доменной логики добавляет косвенность без обеспечения изоляции.

Это также хорошая цель при модернизации устаревшей системы.

Вы можете вырезать одну возможность за раз из монолита, дать ей чистое ядро с портами и постепенно перенаправлять трафик к ней, что является подходом, описанным в нашем руководстве по паттерну strangler fig. Команды, планирующие такой вид миграции в рамках более крупного построения продукта, могут ознакомиться с услугами разработки программного обеспечения Netguru.

Плюсы и минусы гексагональной архитектуры

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

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

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

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

Гексагональная архитектура против чистой архитектуры и архитектуры лука

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

Архитектура лука, описанная Джеффри Палермо в 2008 году, представляет приложение в виде концентрических слоев вокруг доменной модели, с доменными сервисами и приложенческими сервисами в своих собственных кольцах. Чистая архитектура, опубликованная Робертом К. Мартином в 2012 году, использует аналогичный набор колец (сущности, сценарии использования, интерфейсные адаптеры, фреймворки и драйверы) и строгое правило, что зависимости исходного кода указывают только внутрь.

Гексагональная архитектура, как ее определил Кокборн в 2005 году, является наименее предписывающей из трех. Она ничего не говорит о слоях внутри ядра. Она только отделяет внутреннее от внешнего и называет два вида адаптеров: адаптер-привод вызывает ядро, а адаптер-привод вызывается им.

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

Гексагональная архитектура в проектировании микросервисов

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

Кросс-контекстная интеграция — это то, где команды сталкиваются с трудностями. Когда один сервис вызывает API другого или потребляет события от устаревшей системы с другим именованием и инвариантами, ядру нужна антикоррупционная прослойка на границе. Это адаптер-переводчик за портом, а не просто REST-клиент. Он становится единой точкой, где два ограниченных контекста действительно встречаются.

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

Замена адаптеров также остается дешевой на уровне сервиса. Если порт чистый, перенос адаптера-привода из очереди одного облачного провайдера в очередь другого — это новая работа на границе, а не переписывание домена. Сложная часть — это сдержанность: знать, где провести границу ограниченного контекста, а не писать еще один класс адаптера.

FAQ: Ответы на вопросы о гексагональной архитектуре

Является ли гексагональная архитектура DDD?

Нет, но они хорошо работают вместе. Доменное проектирование — это способ моделирования бизнес-домена (ограниченные контексты, агрегаты, повсеместный язык). Гексагональная архитектура — это способ структурирования приложения так, чтобы модель не зависела от инфраструктуры. Многие практики DDD используют гексагон для каждого ограниченного контекста.

Каковы недостатки использования гексагональной архитектуры?

Она добавляет интерфейсы, код маппинга и тестовые дубликаты, что замедляет первоначальное развитие и усложняет кривую обучения для команд, новых для инверсии зависимостей. Для небольших CRUD-приложений с одной базой данных и без ожидаемых изменений инфраструктуры эта накладная стоимость часто перевешивает преимущества.

По-прежнему ли актуальна гексагональная архитектура?

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

Как DTO используются в гексагональной архитектуре?

DTO передают данные через границу порта, чтобы доменное ядро никогда не зависело от формата запроса ведущего адаптера или схемы персистентности ведомого адаптера. REST-контроллер преобразует свой JSON-пayload в команду или DTO, а приложенческий слой преобразует это в доменные объекты. Пропуск этого преобразования приводит к утечке деталей фреймворка в бизнес-логику.

Гексагональная архитектура — это то же самое, что и микросервисы?

Нет. Гексагональная архитектура структурирует внутренности одного приложения, а микросервисы разделяют систему на независимо развертываемые сервисы. Вы можете построить монолит или микросервис с гексагональной архитектурой внутри, и многие команды делают и то, и другое.

Применение гексагональной архитектуры на практике

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

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

О чём эта статья

Ещё в разделе «Разработка ПО»

Все →

Ещё от Netguru