Hexagonal (Ports & Adapters) и Onion
Общее
Гексагональная архитектура (Hexagonal Architecture, она же Ports & Adapters — «порты и адаптеры») и луковая архитектура (Onion Architecture) — два родственных архитектурных паттерна, объединённых одной идеей: бизнес-логика приложения образует ядро, полностью изолированное от внешнего мира, а всё остальное — пользовательский интерфейс, базы данных, очереди сообщений, сторонние сервисы — подключается к ядру снаружи, через строго определённые границы. Авторы паттернов — Алистер Коубёрн (Alistair Cockburn; эссе Hexagonal Architecture, каноническая версия датируется 2005 годом) и Джеффри Палермо (Jeffrey Palermo; цикл записей The Onion Architecture, 2008) — описали одну и ту же конструкцию в разных метафорах: Коубёрн изобразил приложение гексагоном с «портами», к которым «адаптеры» подключаются симметрично с любой стороны, Палермо — «луковицей» концентрических слоёв, сквозь которые все зависимости направлены строго внутрь. Механика у обоих одна: принцип инверсии зависимостей, применённый не к отдельным классам, а к границам всего приложения. Именно эта общая механика делает паттерны прямыми предшественниками Clean Architecture, а их названия в индустрии регулярно употребляются чуть ли не как синонимы.
Исторически оба паттерна — среднее звено долгой эволюции, начавшейся задолго до них. Идея «ядра и периферии» восходит к архитектуре BCE (Boundary-Control-Entity) Ивара Якобсона, описанной в книге Object-Oriented Software Engineering (1992): в любой системе Якобсон различал граничные объекты, общающиеся с внешним миром, и внутреннюю логику, которую эти объекты лишь обслуживают, — периферия не должна просачиваться в ядро. Классическая слоистая архитектура довела разделение ответственности до индустриального стандарта, но закрепила и врождённый дефект: домен в ней располагается над слоем доступа к данным и потому зависит от него — в бизнес-логику просачиваются схема базы, SQL и детали ORM. Hexagonal и Onion появились как прямой ответ на этот дефект: оба паттерна сохраняют идею слоёв, но переворачивают зависимость между ядром и инфраструктурой. Завершает линию Clean Architecture Роберта Мартина (пост The Clean Architecture, 2012; книга, 2017): Мартин честно называет Коубёрна и Палермо своими предшественниками и открывает канонический пост признанием, что все перечисленные подходы «суть вариации одной темы».
| Веха | Автор, год | Что добавила к идее «ядра и периферии» |
|---|---|---|
| BCE | Ивар Якобсон, 1992 | Граничные объекты отделены от логики управления и сущностей; периферия обслуживает ядро |
| Слоистая архитектура | Дейкстра, Парнас и традиция структурного проектирования, 1968–1990-е | Слои как индустриальный стандарт; но домен оказался над доступом к данным и зависим от него |
| Hexagonal (Ports & Adapters) | Алистер Коубёрн, 2005 | Порты и адаптеры; симметрия доставок; ядро одинаково вызывается из HTTP, CLI и теста |
| Onion Architecture | Джеффри Палермо, 2008 | Концентрические кольца вокруг доменной модели; зависимости строго внутрь |
| DCI | Джеймс Коплиен и Трюгве Реенскоуг, 2009 | Разделение «что система есть» (домен) и «что система делает» (сценарии) |
| Clean Architecture | Роберт Мартин, 2012 / 2017 | Синтез: четыре кольца, правило зависимостей, связь с SOLID |
Коубёрн — один из авторов Agile-манифеста и исследователь методологий разработки — пришёл к своему паттерну из практики консультирования: в проектах конца 1990-х он раз за разом видел одну и ту же болезнь, когда одну и ту же бизнес-логику должны были одинаково вызывать человек через графический интерфейс, другая система через интеграционный шлюз, пакетное задание по расписанию и автоматический тест, — а могла она, как правило, только та, под которую была написана. В эссе цель сформулирована прямо: архитектура должна допускать равное обращение с пользователями, программами, автоматическими тестами и пакетными заданиями и позволять разрабатывать и тестировать приложение в изоляции от его будущих устройств и баз данных. Терминология заимствована из мира операционных систем и железа: порт — по аналогии с портом ОС, логическая точка подключения с протоколом, определённым на стороне ядра; адаптер — по аналогии с адаптером периферийного устройства, переводящим внешний мир на язык этого протокола.
Сама шестиугольная форма — сознательная условность, о которой Коубёрн предупреждает сам: число шесть никакой нагрузки не несёт, важна лишь фигура «с более чем двумя сторонами». У многоугольника нет «верха» и «низа», поэтому диаграмма не подталкивает к иллюзии, будто интерфейс «главнее» базы данных или наоборот, — в отличие от прямоугольника слоёв, чья геометрия молча кодирует иерархию. Палермо же — американский .NET-архитектор и консультант — публиковал свой паттерн в 2008 году циклом из четырёх записей в блоге, и его исходная аудитория была конкретной: корпоративная разработка на C#, где к концу 2000-х стандартная слоистая схема выродилась в шаблон «доменная сущность = строка таблицы». Модели, обвешанные атрибутами ORM и знающие о соединениях с базой, не давали ни изолированно проверить бизнес-правила, ни сменить хранилище; ответ Палермо сохранил порты и адаптеры Коубёрна, но сместил акцент с равноправия внешних акторов на внутреннее устройство ядра — доменную модель с поведением в центре и всю инфраструктуру на внешнем контуре.
Общая механика обоих паттернов — инверсия зависимостей на уровне приложения, то есть Dependency Inversion Principle из принципов SOLID, поднятый с уровня классов до уровня архитектуры. Ядро объявляет интерфейсы своих потребностей — хранилище, уведомления, платежи — в собственных терминах; инфраструктура реализует эти интерфейсы и потому зависит от ядра, а не наоборот. Вызов в рантайме при этом вполне может идти наружу (сценарий сохраняет заказ в базу), но зависимость в исходном коде направлена внутрь. Из этой механики напрямую следует главный практический выигрыш обоих паттернов: ядро тестируется без базы данных и HTTP — на место адаптеров подставляются заглушки, тесты выполняются за доли секунды и проверяют именно бизнес-правила, а не склеивающий код фреймворка.
Второй общий тезис стоит зафиксировать отдельно, потому что он отличает оба паттерна от классической слоистой традиции: домен изолирован от инфраструктуры и доставки. Ядро не знает ни о существовании HTTP, ни о схеме хранения; смена базы данных, протокола или UI-фреймворка не является для него событием. Вся разница между Hexagonal и Onion — в том, на что каждый из них смотрит пристальнее: Коубёрн — на границу и симметрию внешних акторов, Палермо — на слоистое устройство самого ядра. Подробный разбор обоих ракурсов — ниже; их синтез в виде четырёх колец и правила зависимостей — в статье о Clean Architecture.
Стоит сразу снять и частую путаницу: оба паттерна — стиль организации кода, а не стиль развёртывания. Они ничего не говорят о том, монолитна система или распределена, и одинаково применяются внутри монолита и внутри каждого отдельного микросервиса; граница проходит не между процессами, а между частями кодовой базы. Это наследие слоистой традиции, описывавшей уровни одного приложения, — но перевёрнутое по направлению зависимостей.
Названия паттернов заслуживают отдельной оговорки, потому что регулярно сбивают с толку. «Гексагональная» не означает, что компонентов должно быть шесть или что приложение делится на шесть частей: форма многоугольника выбрана лишь для того, чтобы у схемы не было «верха» и «низа» — сам паттерн носит двойное имя Ports & Adapters, и оно точнее передаёт механику. «Луковая» отсылает к способу чистки луковицы: чтобы добраться до сердцевины, приходится снимать слои один за другим, и порядок этот не случаен.
В этой базе знаний паттерны рассматриваются как пара и как предыстория Clean Architecture. Читать их имеет смысл после слоистой архитектуры, от которой оба отталкиваются, и рядом с DDD: тактические паттерны Эванса — сущности, агрегаты, доменные сервисы — естественно наполняют ядро, описанное и Коубёрном, и Палермо, а сам словарь Палермо (Domain Model, Domain Services) прямо оттуда.
Hexagonal: порты и адаптеры
Механика паттерна
Гексагональная архитектура изображает приложение как многоугольник — традиционно шестиугольник, — внутри которого живут доменные объекты и сценарии приложения: всё, что система «есть» и «делает» по существу, без упоминания технологий доставки и хранения. Стены гексагона пронизаны портами — интерфейсами, определёнными в терминах ядра: каждый порт описывает либо сценарий, который можно исполнить, либо потребность ядра, которую нужно удовлетворить. Снаружи к портам подключаются адаптеры — классы, переводящие конкретную технологию в вызовы ядра и обратно: веб-фреймворк в вызов сценария, результат сценария в HTTP-ответ; SQL и ORM — в ответ на запрос хранилища.
драйверы (primary) управляемые адаптеры (secondary)
┌──────────────────────┐ ┌─────────────────┐ ┌────────────────────────┐
│ REST-контроллер │ │ │ │ PostgreSQL-репозиторий │
│ CLI-команда │ primary │ Приложение │secondary│ SMTP-шлюз │
│ Консьюмер очереди │──порт──▶│ и домен │──порт──▶│ Издатель сообщений │
│ Юнит-тест │ │ («гексагон») │ │ Клиент стороннего API │
└──────────────────────┘ └─────────────────┘ └────────────────────────┘
Стрелки на схеме показывают направление вызовов в рантайме: слева акторы запускают ядро, справа ядро обращается к внешнему миру. Направление зависимостей в исходном коде при этом одинаково у всех участников: от адаптеров — к ядру. Этот разрыв между потоком вызовов и потоком зависимостей и есть механизм изоляции, а обеспечивающая его конструкция — интерфейс (порт), принадлежащий ядру, и реализация (адаптер), зависящая от него.
Ключевая деталь, из которой следует всё остальное: порты принадлежат ядру. Интерфейс репозитория объявляет не инфраструктурный модуль, а сам сценарий, которому нужно хранилище; сигнатура порта описана в терминах ядра и меняется только вместе с ним. Инфраструктура подключается к порту, а не наоборот. В этом Hexagonal совпадает и с Onion, и с Clean Architecture — и принципиально отличается от распространённой имитации «интерфейса над драйвером БД», которая по форме абстракционна, а по существу привязана к базе: сигнатуры её методов дышат схемой хранения и живут в инфраструктурном модуле.
Первичные и вторичные порты
Первичные порты (primary ports; в исходном эссе — «левая сторона», в позднейшей литературе — driving) — интерфейсы, через которые внешний мир вызывает приложение. Это сценарии вроде PlaceOrder или CancelSubscription: их сигнатуры описаны на языке предметной области и не содержат ни HTTP-типов, ни SQL. Первичные адаптеры, или драйверы, — то, что эти порты запускает: REST-контроллер, gRPC-сервер, консольная команда, консьюмер сообщений из очереди, пакетное задание, автоматический тест. Равноправие драйверов — не риторика, а рабочее свойство: тест для Коубёрна — такой же полноправный драйвер приложения, как веб-сервер, и это прямо следует из мотивации паттерна.
Вторичные порты (secondary ports; «правая сторона», driven) — интерфейсы, через которые приложение само обращается к внешнему миру: репозиторий заказов, платёжный шлюз, нотификатор. Ядро объявляет их в своих терминах — IOrderRepository, а не OrdersTable — и не знает, кто стоит за реализацией. Вторичные адаптеры, или управляемые, выполняют обращение к конкретной технологии: PostgreSQL-репозиторий поверх ORM, SMTP-шлюз поверх почтового сервера, издатель сообщений поверх брокера, клиент поверх стороннего API.
| Сторона | Порт (в ядре) | Адаптер (снаружи) | Технология |
|---|---|---|---|
| Primary | IPlaceOrder | REST-контроллер | ASP.NET Core, Spring, Django |
| Primary | IPlaceOrder | gRPC-сервер | gRPC, protobuf |
| Primary | IPlaceOrder | Консольная команда | CLI-фреймворк |
| Primary | IPlaceOrder | Консьюмер очереди | Kafka, RabbitMQ |
| Primary | IPlaceOrder | Пакетное задание | cron, планировщик |
| Primary | IPlaceOrder | Автоматический тест | xUnit, JUnit, pytest |
| Secondary | IOrderRepository | Репозиторий | PostgreSQL + ORM |
| Secondary | INotifier | Почтовый шлюз | SMTP |
| Secondary | IPaymentGateway | Платёжный шлюз | Stripe, ЮKassa |
| Secondary | IEventPublisher | Издатель событий | Kafka, RabbitMQ |
Важно увидеть асимметрию вызовов при симметрии зависимостей. Слева актор вызывает ядро; справа ядро вызывает адаптер; но в обоих случаях зависимость в исходном коде направлена от адаптера к ядру: оба типа портов объявлены внутри гексагона, оба адаптера импортируют типы ядра. Сводно:
| Критерий | Primary (driving) | Secondary (driven) |
|---|---|---|
| Направление вызова | Актор вызывает ядро | Ядро вызывает адаптер |
| Что описывает порт | Сценарий приложения | Потребность ядра во внешнем мире |
| Пример порта | IPlaceOrder | IOrderRepository |
| Типичные адаптеры | Контроллер, CLI, тест, консьюмер | Репозиторий, SMTP-шлюз, издатель сообщений |
| Направление зависимости | Адаптер → ядро | Адаптер → ядро |
| Аналог в Clean Architecture | Controller + Use Case | Gateway + Frameworks & Drivers |
| Замена адаптера затрагивает | Только доставку | Только инфраструктуру |
О двух практических решениях, которые принимает каждая команда, стоит сказать заранее, потому что на них спотыкаются чаще всего. Первое — зернистость портов: здравый смысл и Interface Segregation рекомендуют узкие порты под сценарий или конкретную потребность, а не один «широкий» сервисный интерфейс на всё приложение; широкие порты незаметно срастаются с единственным потребителем, и заменяемость, ради которой всё строилось, умирает. Второе — судьба доменных сущностей на границе: чем дальше от ядра, тем больше оснований передавать через порт простые структуры данных (request/response-модели), а не протаскивать наружу сами сущности — иначе любое внутреннее изменение модели начинает ломать внешних потребителей.
Пример: порт, сценарий, адаптеры
Миниатюрный пример на C# (стек выбран для преемственности со статьёй о Clean Architecture) показывает раскладку сил: ядро объявляет оба типа портов и реализует сценарий; всё технологичное остаётся за пределами гексагона.
// ── Ядро (внутри гексагона) ───────────────────────────────────
// Первичный порт: сценарий на языке домена, без HTTP и SQL
public interface IPlaceOrder
{
PlaceOrderResult Execute(int customerId, IReadOnlyList<int> itemIds);
}
// Вторичный порт: потребность ядра во внешнем мире
public interface IOrderRepository
{
Customer? FindCustomer(int id);
void Save(Order order);
}
// Реализация сценария: знает только домен и порты
public class PlaceOrder : IPlaceOrder
{
private readonly IOrderRepository _repository;
public PlaceOrder(IOrderRepository repository) => _repository = repository;
public PlaceOrderResult Execute(int customerId, IReadOnlyList<int> itemIds)
{
var customer = _repository.FindCustomer(customerId);
if (customer is null)
return PlaceOrderResult.Fail("customer not found");
var order = customer.PlaceOrder(itemIds); // бизнес-правила — в домене
_repository.Save(order);
return PlaceOrderResult.Ok(order.Id);
}
}
// ── Адаптеры (снаружи гексагона) ──────────────────────────────
// Драйвер: HTTP-запрос → вызов первичного порта
[HttpPost("/orders")]
public IActionResult PlaceOrder([FromBody] PlaceOrderDto dto)
{
var result = _placeOrder.Execute(dto.CustomerId, dto.ItemIds);
return result.Success ? Ok(new { orderId = result.OrderId })
: BadRequest(new { error = result.Error });
}
// Управляемый адаптер: вторичный порт поверх PostgreSQL
public class PgOrderRepository : IOrderRepository
{
private readonly AppDbContext _db; // ORM, соединения, SQL — всё здесь
public PgOrderRepository(AppDbContext db) => _db = db;
public Customer? FindCustomer(int id) => _db.Customers.Find(id);
public void Save(Order order) => _db.Orders.Add(order);
}
Направление зависимостей в примере однозначно: контроллер и PgOrderRepository импортируют типы ядра (IPlaceOrder, IOrderRepository, PlaceOrderResult), ядро не импортирует ничего внешнего. Компоновка — связывание портов с адаптерами — выполняется на самом краю, в точке сборки приложения (в простейшем случае — в Main), и ядро о ней не знает. В реальном проекте у одного первичного порта обычно несколько адаптеров (REST + CLI + тест), а у ядра — несколько вторичных портов (хранилище, платежи, события); всё это — ровно те сменяемые детали, ради которых паттерн и строится. Консольный драйвер того же порта выглядит так:
// Ещё один драйвер того же первичного порта: консольная команда
public class PlaceOrderCommand : ConsoleCommand
{
private readonly IPlaceOrder _placeOrder;
public PlaceOrderCommand(IPlaceOrder placeOrder) => _placeOrder = placeOrder;
public int Invoke(int customerId, int[] itemIds) // ядро не менялось
{
var result = _placeOrder.Execute(customerId, itemIds);
return result.Success ? 0 : 1;
}
}
Задача Коубёрна и симметрия сторон
Задача, которую решал Коубёрн, формулировалась из боли. Приложение конца 1990-х жёстко связывало логику со способом её доставки: бизнес-правила жили в обработчиках кнопок и HTTP-хендлерах, и добавить вторую доставку — консольную утилиту для оператора, интеграцию с другой системой, автотест — означало продублировать логику или выкапывать её из недр интерфейса. Гексагон делает ядро доставко-агностичным: один и тот же PlaceOrder запускается HTTP-запросом, консольной командой, сообщением из очереди и юнит-тестом — меняется только адаптер, ядро не замечает подмены. Приложение, по Коубёрну, вообще не знает и не должно знать, кто и что его вызывает.
Тест заслуживает отдельного акцента, потому что для Коубёрна он — не довесок, а полноправный драйвер:
// Юнит-тест как драйвер: ядро без базы данных и веб-сервера
[Fact]
public void PlaceOrder_saves_order_for_valid_customer()
{
var repository = new FakeOrderRepository(); // заглушка вторичного порта
var useCase = new PlaceOrder(repository);
var result = useCase.Execute(42, new[] { 1, 2, 3 });
Assert.True(result.Success);
Assert.Single(repository.Saved);
}
Такой тест выполняется за доли миллисекунды, не требует окружения и ломается только от изменения бизнес-правил — а не от смены версии СУБД, схемы базы или формата сериализации. Плотный слой таких тестов вокруг ядра — самый прямой и самый ощутимый выигрыш паттерна; интеграционные тесты при этом не исчезают, но переезжают к адаптерам и проверяют уже не логику, а склейку.
Центральное прозрение Коубёрна — симметрия: пользовательский интерфейс и база данных — равноправные адаптеры по разные стороны ядра. В классической слоистой диаграмме UI расположен «сверху», база — «снизу», и эта геометрия неявно кодирует и иерархию важности, и направление интеграции: логика пишется «под интерфейс», поверх «базы». Гексагон убирает обе иллюзии: у сторон многоугольника нет верха и низа, адаптеры равны по определению. Практическое следствие — дешевизна расширения: добавить gRPC-фронтенд рядом с REST, вторую очередь рядом с первой или батч-задание рядом с вебом — значит добавить ещё один адаптер, не тронув ядро; база данных перестаёт быть фундаментом, на котором стоит всё, и становится периферийным устройством, подключённым через порт.
Onion: слои-луковицы
Механика: кольца вокруг домена
Луковая архитектура изображает то же самое приложение концентрическими кольцами. В центре — Domain Model: сущности и агрегаты с поведением, инварианты бизнес-правил — самый стабильный и ценный код системы. Следующее кольцо — Domain Services: операции над несколькими сущностями, не помещающиеся целиком ни в одну из них. Вокруг — Application Services: сценарии и оркестрация, точка, где ядро объявляет интерфейсы инфраструктуры. Внешний контур — вся Infrastructure (ORM, СУБД, очереди, сторонние API), UI и даже Tests: всё изменчивое и технологичное. Единственное правило то же, что позже сформулирует Мартин: зависимости направлены строго внутрь — каждое кольцо может ссылаться только на кольца ближе к центру.
┌────────────────────────────────────────────────────────────────┐
│ Outer: Infrastructure, UI, Tests, Externalized Interfaces │
│ (ORM, СУБД, брокеры сообщений, почта, сторонние API) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Application Services: сценарии и оркестрация │ │
│ │ (объявляют интерфейсы инфраструктуры) │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Domain Services: операции над моделью │ │ │
│ │ │ (интерфейсы репозиториев и внешних сервисов) │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────────────┐ │ │ │
│ │ │ │ Domain Model │ │ │ │
│ │ │ │ (сущности, агрегаты, инварианты) │ │ │ │
│ │ │ └────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
все зависимости направлены строго внутрь ──▶
| Кольцо | Содержимое | Кто может ссылаться |
|---|---|---|
| Domain Model | Сущности, агрегаты, объекты-значения, инварианты | Domain Services, Application Services, внешний контур |
| Domain Services | Операции над несколькими сущностями домена | Application Services, внешний контур |
| Application Services | Сценарии, оркестрация, объявление интерфейсов инфраструктуры | Внешний контур |
| Внешний контур | ORM, СУБД, очереди, UI, тесты, сторонние API | Никто из внутренних колец |
Правила Палермо
Из цикла 2008 года выводится компактный набор правил, которые Палермо формулирует для .NET-мира, но которые переносятся на любой стек. Приложение строится вокруг объектной модели с поведением: центр системы — доменные объекты с методами и инвариантами, а не «сущности-строки таблиц», обслуживаемые процедурами сервисного слоя. Внутренние кольца объявляют интерфейсы, внешние их реализуют: направление реализации всегда от периферии к центру, сцепление (coupling) направлено к центру — формулировка Палермо. Код внутренних колец не ссылается на внешние ни в каком виде: домен не импортирует ни ORM, ни типы веб-фреймворка, ни конфигурацию инфраструктуры. Наконец, ядро собирается отдельно от инфраструктуры: в .NET это отдельная сборка, физически не ссылающаяся на сборки данных и интерфейса, — нарушение правила ловится компилятором, а не только код-ревью.
Из правил следуют два важных уточнения. Первое: инфраструктура в Onion — не «нижний слой», а внешний контур; база данных, почта, брокеры и UI находятся на одном расстоянии от центра и в равной мере являются деталями — та же симметрия, которую Коубёрн выражал формой гексагона. Второе: тесты живут во внешнем контуре, но тестируют центр напрямую — без базы данных и веб-сервера; Палермо прямо опирается на инверсию зависимостей и IoC-контейнеры как рабочий инструмент склейки колец. По сути Onion — это порты и адаптеры Коубёрна, переложенные на язык слоёв и сборок, с добавленной онтологией самого ядра.
Физическая изоляция ядра — не .NET-эксклюзив: тот же приём работает в любом стеке, где есть модули или пакеты, — отдельный Maven-модуль в Java, отдельный пакет в TypeScript, отдельная библиотека в Go. Там, где границы сборок недоступны или неудобны, направление зависимостей проверяют статическими анализаторами — ArchUnit и NetArchTest для JVM и .NET, dependency-cruiser для TypeScript, import-linter для Python: правило «домен не импортирует инфраструктуру» превращается в автоматическую проверку в CI, а не в предмет памяти и код-ревью.
Отличия акцентов от Hexagonal
Механически Onion наследует Hexagonal всё: и порты с адаптерами, и инверсию зависимостей, и тестируемость без инфраструктуры. Различие — в фокусе. Гексагональная архитектура сосредоточена на границе и симметрии внешних акторов; внутреннее устройство гексагона паттерн не регламентирует — домен и сценарии могут лежать внутри как угодно. Луковая архитектура, наоборот, детализирует именно ядро: Domain Model → Domain Services → Application Services — явная слоистая модель домена, заимствованная из словаря DDD Эрика Эванса. Слои Палермо отвечают на вопрос «как устроено ядро», порты Коубёрна — «как ядро общается с миром»; зрелая реализация отвечает на оба вопроса сразу.
Соответствие словарей Onion и DDD почти взаимное, что и сделало связку «тактические паттерны DDD внутри луковицы» канонической в .NET-сообществе:
| Элемент Onion | Элемент DDD | Пример |
|---|---|---|
| Domain Model | Сущности, агрегаты, объекты-значения | Order, Customer с инвариантами |
| Domain Services | Доменные сервисы | PricingService над несколькими агрегатами |
| Application Services | Прикладной слой | Оркестрация сценария PlaceOrder |
| Интерфейсы инфраструктуры | Репозитории, шлюзы | IOrderRepository, IPaymentGateway |
| Внешний контур | Инфраструктура | Реализации репозиториев, брокеры, UI |
Есть и различие происхождения, объясняющее акценты. Hexagonal вырос из задачи множественных доставок и интеграций — одно ядро, много способов вызова; Onion — из боли корпоративной .NET-разработки, где домен растворялся в ORM и «сущностях-таблицах», и главным лекарством была физическая изоляция ядра в отдельную сборку. Отсюда и историческая география популярности: Onion прижился прежде всего в экосистеме .NET, Hexagonal — в Java/JVM-сообществе и интеграционных проектах; к концу 2010-х оба растворились в общем потоке, который Мартин оформил как Clean Architecture.
Hexagonal vs Onion vs Clean
На уровне механизма все три подхода совпадают: домен в центре, детали снаружи, зависимости внутрь. Спорить «Hexagonal или Clean» бессмысленно — это вопрос словаря и акцентов, а не дизайна. Таблица ниже сводит различия; подробный разбор колец и правила зависимостей Clean Architecture — в соответствующей статье.
| Hexagonal (Ports & Adapters) | Onion Architecture | Clean Architecture | |
|---|---|---|---|
| Автор | Алистер Коубёрн | Джеффри Палермо | Роберт Мартин |
| Год | 2005 | 2008 | 2012 (пост) / 2017 (книга) |
| Ключевая метафора | Гексагон с портами по сторонам | Луковица концентрических слоёв | Концентрические кольца |
| Механизм изоляции | Порты (интерфейсы ядра) + адаптеры снаружи | Зависимости строго внутрь; ядро — отдельная сборка | Правило зависимостей + boundary-интерфейсы |
| Устройство ядра | Не регламентируется: приложение + домен | Domain Model → Domain Services → Application Services | Entities → Use Cases → Interface Adapters |
| Отношение к фреймворкам и БД | Равноправные адаптеры, детали | Внешний контур; домен их не видит | «Детали»; самое внешнее кольцо |
| Симметрия UI и БД | Явная: обе стороны гексагона | Слабее: акцент на домене, а не на сторонах | Кольца без «верха» и «низа» |
| Историческая среда применения | Консалтинг, Java/JVM, интеграции | Корпоративный .NET (ORM, IoC) | Вся индустрия, включая Android |
| Связь с принципами | Симметрия интерфейсов, тестируемость | Прямая опора на DIP и IoC | Продолжение SOLID на уровень приложения |
| Канонический текст | Эссе Hexagonal Architecture | Цикл записей The Onion Architecture | Пост The Clean Architecture, книга Clean Architecture |
Полезно и словарное соответствие ролей — оно показывает, что, называя одни и те же элементы по-разному, все три подхода описывают одну конструкцию:
| Роль | Hexagonal | Onion | Clean Architecture |
|---|---|---|---|
| Бизнес-правила | Ядро гексагона (домен) | Domain Model + Domain Services | Entities |
| Сценарий приложения | Первичный порт | Application Service | Use Case (Interactor) |
| Доступ к данным | Вторичный порт + адаптер | Интерфейс в ядре, реализация в Infrastructure | Gateway (Interface Adapters → Frameworks) |
| REST-точка входа | Первичный адаптер | Внешний контур (UI) | Controller (Interface Adapters) |
| ORM и СУБД | Вторичный адаптер | Infrastructure | Frameworks & Drivers |
| Компоновка | Точка сборки приложения | IoC-контейнер / Composition Root | Main |
Чтение таблиц подтверждает: различия лежат в словаре и акцентах, а не в механике. Hexagonal даёт словарь для границ (порты и адаптеры) и культ равноправия доставок; Onion — словарь для ядра (кольца домена) и дисциплину физических сборок; Clean Architecture упаковывает и то и другое в четыре кольца с единым правилом зависимостей и связывает с SOLID, превращая набор приёмов в систему принципов. Общий стержень всех трёх — инверсия зависимостей: без DIP «зависимости внутрь» технически невозможны, бизнес-логика неизбежно импортирует драйвер БД. Показательно, что сам Мартин открывает канонический пост перечислением предшественников — Hexagonal и Onion входят в этот список наряду с BCE Якобсона и DCI. Инженер Херберту Граса (Herberto Graça) в известном цикле статей о наведении порядка в архитектурах показал, что перечисленные подходы не конкурируют, а складываются в одну схему: DDD поставляет модель, порты и адаптеры — механику границ, Onion и Clean — организацию слоёв.
Практический вывод — тот же, к которому приходит статья о Clean Architecture: команда выбирает один словарь, фиксирует его в архитектурных решениях (ADR) и не смешивает терминологии в одном проекте. Смешение («порт», «кольцо», «интерактор» и «шлюз» в одном кодстайле) не ломает механику, но дорого обходится онбордингу и ревью: люди спорят о словах, имея в виду одну и ту же конструкцию.
Плюсы
Тестируемость без инфраструктуры. Главный и самый ощутимый выигрыш. Ядро проверяется чистыми юнит-тестами: на место адаптеров подставляются заглушки, ни СУБД, ни веб-сервера, ни сети; тесты быстрые, детерминированные и не «мигают» из-за окружения. Для Коубёрна тест — первоклассный драйвер, поэтому регрессионная сетка естественным образом строится там, где ценность выше всего, — вокруг бизнес-правил.
Заменяемость адаптеров и деталей. Смена СУБД, протокола, почтового сервиса или ORM затрагивает один адаптер и точку сборки, не трогая ядро. Добавление новой доставки — CLI рядом с HTTP, gRPC рядом с REST, обработчик очереди — тоже локальная операция. Полные замены базы случаются реже, чем обещают лекции, но частичные операции того же класса происходят постоянно, и именно их стоимость паттерны радикально снижают.
Независимость от фреймворков. Фреймворк — подключённая библиотека на периферии, а не каркас, в который вписан код: Spring, ASP.NET или Django обслуживают адаптеры, но не определяют устройство ядра. Это устраняет vendor lock-in на уровне кода и защищает от главного риска экосистем — вынужденных миграций из-за окончания поддержки.
Явные границы. Порты — это явный, документированный контракт ядра: что система делает (первичные порты) и что ей нужно от мира (вторичные). Направление зависимостей проверяется статически — сборками, ArchUnit, dependency-cruiser — и нарушение ловится автоматически, а не обнаруживается через три года. Граница, которую можно проверить, живёт дольше границы, о которой просто помнят.
Параллельная работа команд. Стабильные интерфейсы ядра — точки стыковки: команда домена развивает сценарии и модель, команда инфраструктуры — адаптеры, договорившись о портах заранее. Структура кода поддерживает структуру команды, а не конфликтует с ней.
Предсказуемая структура и дешёвый онбординг. Соглашение о портах и кольцах интернационально: разработчик, знакомый с паттернами, открывает любой построенный на них проект — от бэкенда на Java до мобильного приложения — и мгновенно знает, где бизнес-правила, где сценарии, где клеящий код. Общий словарь снижает bus-factor, а спор о структуре превращается в проверяемый аргумент: «это нарушение направления зависимостей» — факт, а не вопрос вкуса.
Долгоживущий домен и отложенные решения. Ядро, изолированное от деталей, переносится между платформами, переиспользуется несколькими приложениями предприятия и десятилетиями накапливает ценность, вместо того чтобы растворяться в магии фреймворка конкретного года. Систему можно начинать строить, ещё не выбрав окончательно СУБД и способ доставки: внутренние кольца не зависят от этих решений, и их можно принять позже, когда появится информация.
Минусы
Стоимость входа. Паттерны требуют инженерной зрелости: понимания инверсии зависимостей и DI, привычки писать тесты с заглушками, дисциплины границ. Команда без этого фундамента получит дорогую имитацию — те же связи, только разнесённые по папкам и украшенные интерфейсами.
Бойлерплейт и маппинг. Интерфейсы портов, DTO, мапперы, точка сборки — постоянная плата за изоляцию. Типичная болезнь первых месяцев — «интерфейс ради интерфейса»: абстракция заводится на каждый класс без осмысленной причины замены и становится чистой ценой без выгоды. В простых системах маппинг легко съедает больше кода, чем бизнес-логика, ради которой всё затевалось.
Избыточность для простых CRUD. Подавляющее большинство приложений — формы над базой данных без сколько-нибудь сложных правил. Для них гексагон и луковица — пустой ритуал: сценарий без правил — транзит данных, порт к единственной таблице — лишняя косвенность. Честный controller → service → repository решает ту же задачу в разы дешевле.
Риск карго-культа. Популярность паттернов — особенно после успеха Clean Architecture — породила массовое копирование формы без содержания: папки domain/data/presentation без правила зависимостей, «гексагон» как логотип в README. Без автоматической проверки направления зависимостей структура деградирует за пару спринтов в распределённый по папкам большой шар грязи — хуже честного монолита, потому что создаёт иллюзию порядка.
Словарная путаница. Порты, адаптеры, кольца, слои, интеракторы, шлюзы — три словаря одной идеи, и команды регулярно тратят споры на «правильный» термин, смешивая концепции из разных статей. Лекарство простое: выбрать один словарь и зафиксировать его в архитектурных решениях, а спорить о дизайне, а не о словах.
Не решают системных вопросов. Оба паттерна — стиль организации кода одного приложения, и только. Они не отвечают, как масштабировать нагрузку, резать систему на сервисы, обеспечивать транзакции между ними, версионировать контракты и наблюдать за системой в рантайме. Безупречно изолированное ядро внутри каждой части совместимо и с провальной архитектурой целого.
Когда применять
Сложная доменная логика. Главный критерий — количество и изменчивость бизнес-правил: расчёты тарифов и скидок, лимиты и инварианты, состояния и переходы, законодательные требования. Чем больше правил и чем чаще они меняются, тем быстрее окупается дисциплина границ. Типичные кандидаты — биллинг, страхование, логистика, трейдинг.
Много доставок и интеграций. Конкретные видимые причины: логика, вызываемая одновременно из HTTP, CLI и очереди; несколько внешних систем вокруг ядра (платежи, документы, нотификации); несколько интерфейсов доступа к одной логике — web, API, партнёрский шлюз. Каждая доставка и каждая интеграция — это адаптер; ядро при этом остаётся одним.
Долгоживущий продукт. Горизонт жизни в годы и десятилетия почти гарантирует ротацию технологий доставки и хранения: UI-фреймворки приходят и уходят каждые несколько лет, ORM и версии платформ — ещё чаще. Если код переживёт эту ротацию, изоляция ядра окупается с процентами; для одноразового кода — прототипов, демо, временных лендингов — платить за ротацию вперёд бессмысленно.
Регрессионная защита бизнес-правил. Если ошибка в логике стоит дорого — финансы, медицина, договорные обязательства, — быстрый детерминированный юнит-тест на ядро остаётся самым дешёвым способом получить плотную сетку тестов именно на правила, а не на инфраструктуру вокруг них.
Организационная готовность. Честный критерий: паттерны требуют от команды базовых навыков — инверсии зависимостей, DI, тестов с заглушками. Если их нет, сначала выгоднее вырастить культуру на простой структуре, а границы вводить, когда команда к ним готова.
Когда избыточны. Зеркальный список: CRUD-приложения и админки без правил; прототипы и MVP, чья судьба — быть выброшенными; системы с горизонтом жизни в месяцы; маленькие команды на простом домене без перспективы роста сложности. Число слоёв и портов должно соответствовать числу разных причин изменения, а не эстетике диаграммы.
Применять частично — нормальная практика. Оба паттерна допускают эволюционное внедрение: сначала изолировать только самое ценное ядро расчётов, оставив CRUD-обвязку «грязной»; границы дорезать по мере роста боли. Управлять этим процессом удобно в рамке эволюционной архитектуры: границы проводятся там, где изменчивость доказана, и удерживаются fitness-функциями в CI.
Связанные статьи
- Слоистая архитектура (Layered / N-tier) — исторический предшественник: те же слои, но с зависимостями сверху вниз; Hexagonal и Onion переворачивают зависимость между доменом и инфраструктурой.
- SOLID — пять принципов проектирования; Dependency Inversion — механизм, на котором держатся оба паттерна.
- Clean Architecture — синтез обоих подходов: четыре кольца, правило зависимостей, связь с SOLID.
- Domain-Driven Design (DDD) — содержание ядра: тактические паттерны DDD естественно живут в Domain Model и в словаре Палермо.
- CQRS и Event Sourcing — паттерны организации данных, часто применяемые поверх гексагонального ядра.
- Эволюционная архитектура — управление изменениями: частичное внедрение границ и удержание их fitness-функциями.