Что такое Hexagonal Architecture?

И в чем отличие от слоистой архитектуры
9 мин чтения
Новая фича часто начинается с миграции: добавить таблицу, колонки, внешние ключи. Потом ORM генерирует по таблице сущность, поверх неё появляется сервис, поверх сервиса контроллер. Через год правка одного бизнес-правила задевает три слоя, а тест на возврат билета требует поднять базу данных. Если это похоже на твой проект, гексагональная архитектура предлагает начинать с бизнес-логики и подключать всё остальное к ней.

Как слоистая архитектура приводит к проектированию от базы данных

Классическая слоистая архитектура строит код сверху вниз. Представление вызывает бизнес-логику, бизнес-логика вызывает слой доступа к данным, тот работает с базой. Каждый слой зависит от слоя под ним, и в основании лежит база данных.
Слоистая архитектура: представление вызывает бизнес-логику, бизнес-логика — доступ к данным, а тот работает с базой данных
Все стрелки направлены вниз, поэтому изменение схемы базы данных затрагивает каждый слой над ней.
Возьмём продажу билетов в театр. Работа начинается со схемы: таблица tickets с колонками seat, performance_id, status, price. ORM создаёт по ней класс Ticket с теми же полями, геттерами и сеттерами. Правила вроде «нельзя продать уже проданное место» или «вернуть билет можно только до начала спектакля» оседают в TicketService, а иногда и в контроллере.
Мартин Фаулер называет такую модель анемичной. Объекты в ней названы по предметной области, но поведения у них почти нет, это «мешки с геттерами и сеттерами». Логика живёт в сервисах, и дизайн становится процедурным. Ты платишь за доменную модель маппингом и ORM, а её преимуществ не получаешь.
Последствия обычно такие:
  • смена ORM или структуры таблиц задевает бизнес-логику, потому что она зависит от слоя данных;
  • правило возврата билета трудно проверить тестом без базы;
  • слои со временем начинают обходить: контроллер обращается к репозиторию напрямую, минуя сервис.
Том Хомбергс в книге «Get Your Hands Dirty on Clean Architecture» называет это проектированием от базы данных и связывает его с ORM-фреймворками. Аккуратно построенные слои, по его словам, способны отделить доменную логику от веба и хранения, но оставляют слишком много лазеек, если команда не вводит дополнительных правил.

Hexagonal Architecture: домен в центре вместо базы данных в основании

В 2005 году Алистер Кокберн описал этот паттерн и дал ему более точное имя Ports & Adapters. Цель он сформулировал так: приложение должно работать без UI и без базы данных. Такое приложение можно гонять автоматическими регрессионными тестами, запускать при недоступной базе и связывать с другими приложениями без участия пользователя.
Hexagonal Architecture, или Ports & Adapters, делит систему на приложение с бизнес-логикой и внешний мир. Внешний мир подключается к приложению через порты и адаптеры, поэтому бизнес-логика не знает, какие технологии стоят снаружи.
Главная граница у Кокберна проходит между «внутри» и «снаружи» приложения, деления на верх и низ здесь нет.
Сравнение архитектур: слева слои стоят друг на друге над базой данных, справа домен в центре шестиугольника, а UI и база данных снаружи
Слева база данных лежит в основании всех слоёв, справа она стоит снаружи наравне с UI, а в центре оказывается домен.

Слои гексагональной архитектуры и их ответственность

Ник Тьюн в книге о DDD описывает то же устройство: в центре доменный слой, вокруг него слой приложения, снаружи инфраструктура.

Домен

Домен содержит модель предметной области: билет, место, спектакль. Теперь Ticket сам проверяет, можно ли его продать или вернуть, и правило «вернуть только до начала спектакля» живёт в его методе. Домен ничего не знает о базе данных, HTTP и фреймворках.
Гексагональная архитектура: стрелка указывает на домен в центре шестиугольника
Домен находится в самом центре и не касается внешнего мира ни с одной стороны.

Приложение

Слой приложения описывает сценарии использования (use cases): «купить билет», «вернуть билет». Сервис покупки загружает спектакль и место, поручает домену оформить продажу, сохраняет результат и отправляет уведомление. Бизнес-решений он не принимает и только координирует шаги. Тьюн советует делать такие сервисы тонкими и процедурными.
Гексагональная архитектура: стрелка указывает на слой приложения с use cases вокруг домена
Внешние вызовы доходят до домена только через use cases этого слоя.

Порты

Порт описывает разговор приложения с внешним миром в виде интерфейса. Портов два вида:
  • входящий порт задаёт, что можно попросить у приложения: BuyTicketUseCase вызывает контроллер, тест или пакетный скрипт;
  • исходящий порт задаёт, что приложению нужно от внешнего мира: TicketRepository сохраняет билеты, NotificationSender отправляет письмо с билетом.
Интерфейсы портов сформулированы на языке приложения, без упоминания Postgres или SMTP.
Гексагональная архитектура: стрелка указывает на порт на границе слоя приложения
Порты стоят на границе слоя приложения, с обеих сторон шестиугольника.

Адаптеры

Адаптер переводит сигналы конкретной технологии на язык порта. REST-контроллер превращает HTTP-запрос в команду для BuyTicketUseCase. PostgresTicketRepository реализует TicketRepository через SQL, а в тестах вместо него можно подключить хранилище в памяти.
Через границу доменные объекты не передаются: адаптеры обмениваются с приложением DTO. Если театр сменит базу данных, ты напишешь новый адаптер с тем же интерфейсом, а домен и use cases останутся прежними.
Гексагональная архитектура: стрелка указывает на внешнее кольцо адаптеров
Адаптеры образуют внешнее кольцо: слева подключаются UI, API и тесты, справа база и внешние сервисы.

Инверсия зависимостей: почему база данных оказалась снаружи

В слоистой архитектуре сервис вызывает репозиторий и зависит от него. В гексагональной вызов идёт в ту же сторону, а зависимость развёрнута.
Принцип инверсии зависимостей (Dependency Inversion Principle): модули верхнего уровня не зависят от модулей нижнего уровня, и те и другие зависят от абстракций. Абстракции не зависят от деталей, детали зависят от абстракций.
Абстракцией здесь служит порт. Слой приложения объявляет интерфейс TicketRepository, инфраструктура его реализует. Во время покупки управление проходит путь «контроллер → use case → домен → репозиторий → база», но в коде PostgresTicketRepository импортирует интерфейс из слоя приложения, а слой приложения о Postgres не знает.
Гексагональная архитектура: поток выполнения идёт слева направо к базе данных, а зависимости в коде с обеих сторон направлены к домену
Синяя стрелка выполнения проходит через всё приложение до базы, а красные стрелки зависимостей с обеих сторон сходятся к домену.
Поэтому домен и use cases можно тестировать изолированно, без базы и веб-фреймворка.
theater-tickets/
├── domain/
│   ├── Ticket                         # сущность с правилами продажи и возврата
│   ├── Seat
│   └── Performance
├── application/
│   ├── port/
│   │   ├── in/
│   │   │   └── BuyTicketUseCase       # интерфейс
│   │   └── out/
│   │       ├── TicketRepository       # интерфейс
│   │       └── NotificationSender     # интерфейс
│   └── BuyTicketService               # реализация BuyTicketUseCase
└── adapter/
    ├── in/
    │   └── web/
    │       └── TicketController       # вызывает BuyTicketUseCase
    └── out/
        ├── persistence/
        │   └── PostgresTicketRepository   # реализация TicketRepository
        └── notification/
            └── EmailNotificationSender    # реализация NotificationSender

Структура проекта: где интерфейсы, а где реализации

В коде правило выглядит так: порты объявлены интерфейсами внутри приложения, адаптеры реализуют или вызывают их снаружи. Входящий порт реализует use case, исходящий реализует адаптер.

Article's Title

Book design is the art of incorporating the content, style, format, design, and sequence of the various components of a book into a coherent whole. In the words of Jan Tschichold, "Methods and rules that cannot be improved upon have been developed over centuries. To produce perfect books, these rules must be revived and applied." The front matter, or preliminaries, is the first section of a book and typically has the fewest pages.

Article's Title

Book design is the art of incorporating the content, style, format, design, and sequence of the various components of a book into a coherent whole. In the words of Jan Tschichold, "Methods and rules that cannot be improved upon have been developed over centuries. To produce perfect books, these rules must be revived and applied." The front matter, or preliminaries, is the first section of a book and typically has the fewest pages.

Article's Title

Book design is the art of incorporating the content, style, format, design, and sequence of the various components of a book into a coherent whole. In the words of Jan Tschichold, "Methods and rules that cannot be improved upon have been developed over centuries. To produce perfect books, these rules must be revived and applied." The front matter, or preliminaries, is the first section of a book and typically has the fewest pages.
Мой канал в Telegram
Ветчинкин Кирилл
Backend, DDD, Clean Architecture и AI-агенты.
Делюсь опытом и примерами кода.
Архитектура Backend
приложений