Порты и адаптеры
В hexagonal architecture порт — это интерфейс, которым владеет domain. Адаптер — любой компонент, реализующий или вызывающий его. Эта симметрия делает HTTP, CLI и базу данных просто адаптерами на границе domain.
На B2B-платформе заказов при запуске было три канала доставки: HTTP REST API для веб-фронтенда, CLI-инструмент для команды эксплуатации и ночной batch-job, подтягивавший заказы из SFTP-дропа партнёра. Каждый канал содержал собственную копию логики подтверждения заказа. Когда продуктовая команда добавила шаг согласования с отделом биллинга, три инженера обновили три разных кодовых пути. Двое справились. Третий пропустил граничный случай для заказов свыше $10 000. Баг прожил в продакшене шесть недель, потому что никто не знал, что CLI имеет собственную логику подтверждения. Проблема была не в трёх каналах — у приложения не было границы. Не было единственного определения «подтвердить заказ», через которое вызывала бы каждая точка входа. Именно эту проблему Alistair Cockburn описывал в 2005 году, представляя hexagonal architecture: приложению нужна чёткая граница, через которую каждый вызывающий — человек, автоматика или тест — должен проходить через одинаковый интерфейс.
Что такое порт на самом деле
Слово «port» в hexagonal architecture заимствовано из аппаратного обеспечения: порт — это разъём, определённый протокол подключения. В программировании порт — это интерфейс, который определяет само приложение. Это владение и отличает hexagonal architecture от слоистой.
На B2B-платформе заказов приложение могло бы определить порт так:
// Определён внутри приложения — в собственном пакете domain
interface OrderConfirmationPort {
confirmOrder(orderId: string, approverId: string): Promise<ConfirmationResult>;
}Этот интерфейс принадлежит приложению. Он выражает то, что умеет делать приложение, — на языке domain, а не HTTP или CLI. Приложение определяет контракт; внешний мир должен ему соответствовать.
В этом инверсия, отличающая hexagonal от слоистой архитектуры. В стандартном слоистом стеке application layer вызывает OrderRepository — инфраструктурный класс: приложение зависит от infrastructure. В hexagonal приложение определяет IOrderRepository как порт — infrastructure зависит от определения интерфейса приложения. Стрелка зависимости идёт от infrastructure к domain, никогда наоборот.
Что такое адаптер на самом деле
Адаптер — это любой компонент, переводящий между внешним миром и портом. Адаптер либо:
- Вызывает порт (HTTP-контроллер вызывает
OrderConfirmationPort.confirmOrder, когда приходит POST-запрос) - Реализует порт (
PostgresOrderRepositoryреализуетIOrderRepository, удовлетворяя потребности domain в данных)
Ключевое наблюдение: эти две категории симметричны. HTTP-контроллер и PostgreSQL-репозиторий — оба адаптеры: один на левой стороне гексагона (вызывающий), один на правой (вызываемый). Структурно они эквивалентны: каждый — слой трансляции между внешней технологией и интерфейсом порта.
Эта симметрия и делает название архитектуры точным. У гексагона есть грани, и каждая может иметь порт. HTTP-адаптер подключается к одной грани; gRPC-адаптер мог бы подключиться к той же грани (через тот же порт). PostgreSQL-адаптер подключается к другой грани; in-memory-адаптер мог бы подключиться к той же грани. Application core не знает и не заботится, какие адаптеры подключены.
Симметрия: UI и база данных — оба просто адаптеры
Это наиболее практически полезное следствие hexagonal architecture. HTTP-слой не привилегирован — это просто адаптер, переводящий HTTP-запросы в вызовы порта. База данных не привилегирована — это просто адаптер, реализующий порт репозитория. Оба внешние по отношению к application core.
Из этого следует немедленное структурное следствие: можно независимо менять любую сторону. Заменить HTTP-адаптер на gRPC — application core не изменится. Заменить PostgreSQL на DynamoDB — application core не изменится. Добавить CLI-адаптер рядом с HTTP — application core не изменится. Именно этого не хватало B2B-платформе: если бы логика подтверждения заказа жила за единственным OrderConfirmationPort, HTTP-контроллер, CLI-адаптер и batch-адаптер вызывали бы один и тот же порт. Одно исправление бага в одном месте.
▸Почему это работает
Почему Cockburn назвал архитектуру «hexagonal», а не круговой или квадратной? Гексагон не несёт особого значения — шесть граней не обязательное требование. Cockburn выбрал гексагон, чтобы на диаграммах было место для нескольких адаптеров на каждой стороне без сужения формы. Значимая форма в том, что приложение — это замкнутая область с явными точками входа/выхода (портами), а не открытый слоистый стек, где что угодно может импортировать что угодно. Количество граней — удобство для рисования, а не архитектурное ограничение.
Порты как API и SPI приложения
Существуют две категории портов, напрямую соответствующие двум сторонам гексагона:
Primary ports (API — Application Programming Interface) определяют, как внешний мир управляет приложением. Это use case’ы, которые приложение предоставляет. На B2B-платформе: OrderConfirmationPort, InvoiceGenerationPort, BillingApprovalPort. Driving adapters вызывают эти порты.
Secondary ports (SPI — Service Provider Interface) определяют, что приложению нужно от infrastructure. Приложение вызывает эти порты; infrastructure реализует их. На B2B-платформе: IOrderRepository, IPaymentGateway, IEmailNotifier, IEventPublisher. Это в точности dependency-inverted интерфейсы из unit 02 — DIP, применённый на архитектурной границе.
Названия API и SPI пришли из экосистемы Java, но концепция универсальна. Приложение определяет обе стороны: что оно предлагает (API/primary ports) и что ему требуется (SPI/secondary ports). Внешний мир должен соответствовать обеим.
▸lesson.inset.note
Распространённая ошибка — думать, что «порт — это HTTP-эндпоинт» или «порт — это таблица базы данных». Ни то ни другое. Порты определяются на языке domain. OrderConfirmationPort.confirmOrder(orderId, approverId) говорит на языке domain заказов — он ничего не знает о HTTP-глаголах, JSON-телах, SQL-таблицах или топиках очередей сообщений. Перевод между HTTP-языком и domain-языком — это именно работа адаптера. Такое разделение — domain-язык на порту, технологический язык в адаптере — делает приложение тестируемым без HTTP-инфраструктуры и заменяемым без переписывания domain.
Команда определяет IOrderRepository в пакете infrastructure рядом с PostgresOrderRepository. OrderService domain импортирует его оттуда. Является ли это hexagonal architecture и почему?
На B2B-платформе есть HTTP-контроллер, CLI-адаптер и batch-адаптер — все трое активируют use case подтверждения заказа. Как эти адаптеры должны соотноситься с OrderConfirmationPort в hexagonal architecture?
После применения hexagonal architecture к B2B-платформе команда хочет заменить PostgreSQL на document store для коллекции заказов. Какие компоненты изменятся, а какие останутся прежними?
- 01Что делает порт «портом» в hexagonal architecture — чем он отличается от любого другого интерфейса?
- 02На B2B-платформе было три адаптера (HTTP, CLI, batch), каждый со своей копией логики подтверждения. Как hexagonal architecture исправляет это структурно?
- 03Что такое primary ports и secondary ports, и на какой стороне гексагона находится каждый?
Hexagonal architecture решает проблему отсутствия чёткой границы приложения — когда каждая точка входа содержит собственную копию бизнес-логики и одно исправление бага нужно воспроизвести в нескольких кодовых базах.
Порт — это интерфейс, принадлежащий приложению. Он говорит на языке domain и определяет либо то, что предлагает приложение (primary port), либо то, что ему требуется от infrastructure (secondary port). Domain пишет контракт; внешний мир должен ему соответствовать.
Адаптер — слой трансляции между внешней технологией и портом. Driving adapters (HTTP-контроллеры, CLI-раннеры, batch-задания, тест-драйверы) вызывают primary ports для активации приложения. Driven adapters (PostgreSQL-репозитории, Stripe-клиенты, SendGrid-отправители) реализуют secondary ports для удовлетворения инфраструктурных потребностей domain.
Симметрия — HTTP и PostgreSQL оба просто адаптеры на разных сторонах — это практическая выгода. Заменить HTTP-адаптер на gRPC — application core не меняется. Заменить PostgreSQL на document store — application core не меняется. Добавить новую точку входа — логика подтверждения не дублируется. Application core — замкнутая область; порты — её единственные легальные точки входа и выхода; адаптеры — это соответствующие разъёмы, предоставляемые внешним миром.
Эта архитектура — структурный ответ на утечки слоистости из unit 03. Transaction-script trap предотвращается, потому что порт обязывает к определённой границе use case. Layer-skipping предотвращается, потому что infrastructure может достичь core только через реализации портов — infrastructure зависит от интерфейсов domain, а не наоборот. Fat service предотвращается, потому что каждый primary port соответствует ограниченному use case, а не открытому списку методов.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.