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 слои и зависимости — не догма, а набор принципов, которые вы можете адаптировать под конкретный проект. Главное — сохранить направление зависимостей и ясность границ. Тогда код станет предсказуемее, тесты — проще, а команда — увереннее в изменениях.

