open atlas
↑ К треку
Архитектурные паттерны ARCH · 04 · 01

Порты и адаптеры

В hexagonal architecture порт — это интерфейс, которым владеет domain. Адаптер — любой компонент, реализующий или вызывающий его. Эта симметрия делает HTTP, CLI и базу данных просто адаптерами на границе domain.

ARCH Middle ◷ 22 min
Уровень
ОсновыJuniorMiddleSenior

На 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 для коллекции заказов. Какие компоненты изменятся, а какие останутся прежними?

Вспомните перед уходом
  1. 01
    Что делает порт «портом» в hexagonal architecture — чем он отличается от любого другого интерфейса?
  2. 02
    На B2B-платформе было три адаптера (HTTP, CLI, batch), каждый со своей копией логики подтверждения. Как hexagonal architecture исправляет это структурно?
  3. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.