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

Что такое Onion Architecture и зачем она нужна

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

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

Основные слои архитектуры

Структура Onion Architecture традиционно представляется в виде концентрических слоёв, где внутренний слой самый важный и самый независимый. Ниже перечислены типичные уровни с кратким описанием их задач.

Доменный слой (Core)

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

Главное правило — никакой зависимости от фреймворков и баз данных. Интерфейсы, описывающие требуемое поведение, можно объявлять в домене, но реализации должны быть внешними.

Слой приложений (Application)

Этот слой реализует сценарии использования: координирует операции над доменом, организует транзакции и подготавливает данные для внешних слоёв. Задача — не содержать сложной логики домена, а лишь связывать элементы.

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

Инфраструктурный слой (Infrastructure)

Инфраструктура включает реализации интерфейсов: репозитории, адаптеры к базе данных, отправку сообщений, логирование и так далее. Эти классы могут использовать ORM, сетевые клиенты и другие библиотеки.

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

Слой представления и внешние интерфейсы (UI/API)

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

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

Правила зависимостей: в какую сторону смотрят классы

Ключевое правило — зависимости направлены внутрь: внешние слои зависят от внутренних, а не наоборот. Это обеспечивает защиту доменной модели от изменений инфраструктуры.

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

Паттерн Dependency Inversion в действии

Dependency Inversion — центральный принцип. Высокоуровневые модули не должны зависеть от низкоуровневых, оба должны зависеть от абстракций. Это снижает связность и упрощает тестирование.

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

Практическая схема слоёв (таблица)

Ниже небольшая таблица, которая помогает быстро ориентироваться в обязанностях каждого слоя.

Слой Что содержит Зависимость
Домен Сущности, правила, интерфейсы Не зависит от других слоёв
Приложение Сценарии использования, DTO, обработчики Зависит от домена
Инфраструктура Реализации репозиториев, адаптеры Зависит от домена и приложения
UI/API Контроллеры, представления Зависит от приложения

Как это выглядит в коде

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

Тестирование при такой структуре остаётся лёгким: можно подменить реализацию репозитория мок-объектом и проверить поведение сценариев без БД.

Конкретные практики и шаблоны

Есть несколько проверенных приёмов, которые упрощают поддержку Onion Architecture. Они касаются проектирования интерфейсов, названий и границ ответственности.

  • Делите код по ответственности, а не по технологиям: доменные модели в одном проекте, интерфейсы в другом.
  • Не выставляйте ORM-специфические типы в домен: маппинг делайте в инфраструктуре.
  • Используйте явные команды и обработчики для сценариев вместо плотных сервисов с множеством методов.

CQRS и Transaction Script

Onion Architecture хорошо сочетается с разделением команд и запросов (CQRS): команды изменяют состояние, запросы читают данные. Это упрощает управление консистентностью и масштабирование.

В проектах, где бизнес-логика проста, можно предпочесть Transaction Script — короткие транзакционные сценарии в слое приложения. Главное — не смешивать обязанности.

Типичные ошибки и антипаттерны

Самая частая проблема — утечка инфраструктуры в домен. Это проявляется, когда сущности начинают пользоваться типами ORM или когда в домене появляются вызовы HTTP-клиента.

Другой распространённый косяк — чрезмерная дробность: создание сотен проектов и интерфейсов без реальной пользы. Важно найти баланс между отделением ответственности и здравым pragmatism-ом.

Примеры ошибок из практики

В одном проекте я видел, как репозитории возвращали IQueryable в домен. Казалось удобным, но привязало домен к LINQ-провайдерам и усложнило тестирование. Мы заменили это на спецификации и явные методы поиска.

В другом случае контроллеры знали детали транзакций и сами открывали соединения с БД. Это мешало масштабировать и приводило к дублированию кода. Перенос управления транзакциями в слой приложений решил проблему.

Тестирование при Onion Architecture

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

Практический совет — держать набор unit-тестов для домена и слой приложения, а отдельный набор интеграционных тестов для инфраструктуры с реальной БД или тестовым окружением.

Переход от монолита к Onion Architecture

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

На практике полезно выбрать несколько критичных сценариев и рефакторить их по одному. Это создаёт естественные точки интеграции и облегчает откат при необходимости.

Когда Onion Architecture не нужна

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

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

Советы по практической реализации

Придерживайтесь понятных соглашений о наименованиях. Это облегчает ориентирование в большом коде. Я предпочитаю префиксы Domain., Application., Infrastructure., Api. — это работает хорошо в большинстве случаев.

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

Onion Architecture слои и зависимости — не догма, а набор принципов, которые вы можете адаптировать под конкретный проект. Главное — сохранить направление зависимостей и ясность границ. Тогда код станет предсказуемее, тесты — проще, а команда — увереннее в изменениях.