Луковая архитектура (Onion Architecture) — это архитектурный шаблон программного обеспечения, предложенный Джеффри Палермо в 2008 году. Он фокусируется на создании слабосвязанных, модульных и легко тестируемых приложений путем строгого разделения ответственностей между слоями и направлением зависимостей исключительно внутрь, к ядру приложения. Этот подход позволяет изолировать бизнес-логику от инфраструктурных проблем, таких как базы данных, фреймворки и пользовательские интерфейсы.
Луковая архитектура строится на нескольких фундаментальных принципах, которые обеспечивают ее эффективность:
Самый внутренний слой содержит сущности (Entities), объекты-значения (Value Objects) и интерфейсы, определяющие контракты для взаимодействия с внешним миром (например, интерфейсы репозиториев). Этот слой представляет собой чистую бизнес-логику и не должен иметь зависимостей от каких-либо фреймворков или библиотек, связанных с инфраструктурой или представлением.
Внешние слои зависят от абстракций (интерфейсов), определенных во внутренних слоях. Реализации этих интерфейсов предоставляются внешними слоями. Это ключевой аспект, позволяющий отделить бизнес-логику от конкретных технологий.
Приложение структурируется в виде концентрических слоев. Каждый внешний слой может взаимодействовать только с непосредственно примыкающим к нему внутренним слоем через определенные интерфейсы. Это предотвращает "сквозные" зависимости и поддерживает четкую структуру.
Общее представление слоистой архитектуры, схожее с принципами луковой архитектуры.
Луковая архитектура обычно включает следующие основные слои, хотя их количество и названия могут варьироваться в зависимости от конкретного проекта:
Это ядро приложения. Он содержит:
Этот слой не зависит ни от одного другого слоя в приложении.
Этот слой координирует выполнение бизнес-сценариев (use cases). Он содержит:
Прикладной слой зависит только от Доменного слоя.
Этот слой содержит реализации интерфейсов, определенных во внутренних слоях (Доменном и Прикладном). Он отвечает за взаимодействие с внешними системами и технологиями:
Инфраструктурный слой зависит от Доменного и Прикладного слоев (точнее, от их интерфейсов).
Это самый внешний слой, отвечающий за взаимодействие с пользователем или другими системами. Примеры:
Этот слой зависит от Прикладного слоя (обычно через его интерфейсы) и может зависеть от Инфраструктурного слоя для настройки внедрения зависимостей (Dependency Injection).
Наглядное представление слоев в луковой архитектуре и направления зависимостей.
Для лучшего понимания структуры луковой архитектуры и взаимосвязей между ее компонентами, рассмотрим следующую ментальную карту. Она иллюстрирует ключевые слои и их основные элементы, а также направление зависимостей (от внешних слоев к внутренним).
Эта карта подчеркивает, что ядро (Domain Layer) находится в центре и не зависит ни от чего, в то время как внешние слои (Presentation, Infrastructure) зависят от абстракций, предоставляемых внутренними слоями (Application, Domain).
Луковая архитектура обладает рядом важных характеристик, которые делают ее привлекательной для определенных типов проектов. Следующий радарный график визуализирует оценку этих аспектов. Оценка основана на общем восприятии и может варьироваться в зависимости от конкретной реализации и сложности проекта.
Как видно из графика, луковая архитектура высоко ценится за тестируемость, сопровождаемость и слабую связанность. Начальная сложность реализации может быть выше по сравнению с простыми монолитными архитектурами, но это окупается в долгосрочной перспективе для сложных проектов. Масштабируемость также является сильной стороной, хотя может потребовать дополнительных усилий в сочетании с другими паттернами (например, микросервисами). Потенциальные накладные расходы на производительность обычно незначительны при правильной реализации.
Рассмотрим типичную структуру проекта на .NET, использующего луковую архитектуру. Обычно создается несколько проектов в рамках одного решения (Solution):
YourProject.Domain
YourProject.Application
YourProject.Infrastructure
YourProject.Api
(или YourProject.Web
, YourProject.UI
)Содержит сущности и интерфейсы репозиториев.
// YourProject.Domain/Entities/Product.cs
public class Product
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
// Конструктор и методы для бизнес-логики
public Product(string name, decimal price)
{
Id = Guid.NewGuid();
Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentNullException(nameof(name));
Price = price > 0 ? price : throw new ArgumentOutOfRangeException(nameof(price));
}
public void UpdatePrice(decimal newPrice)
{
if (newPrice <= 0) throw new ArgumentOutOfRangeException(nameof(newPrice));
Price = newPrice;
}
}
// YourProject.Domain/Interfaces/IProductRepository.cs
public interface IProductRepository
{
Task<Product> GetByIdAsync(Guid id);
Task AddAsync(Product product);
Task UpdateAsync(Product product);
}
Содержит прикладные сервисы и DTO.
// YourProject.Application/Services/ProductService.cs
public class ProductService // Может реализовывать IProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository ?? throw new ArgumentNullException(nameof(productRepository));
}
public async Task CreateProductAsync(string name, decimal price)
{
var product = new Product(name, price);
await _productRepository.AddAsync(product);
// Дополнительная логика, например, отправка уведомлений
}
public async Task<ProductDto> GetProductAsync(Guid id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null) return null; // или выбросить исключение NotFound
return new ProductDto { Id = product.Id, Name = product.Name, Price = product.Price };
}
}
// YourProject.Application/DTOs/ProductDto.cs
public class ProductDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Содержит реализации интерфейсов, например, для работы с базой данных.
// YourProject.Infrastructure/Data/ProductRepository.cs
// Предполагается использование Entity Framework Core
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _dbContext;
public ProductRepository(ApplicationDbContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
}
public async Task<Product> GetByIdAsync(Guid id)
{
return await _dbContext.Products.FindAsync(id);
}
public async Task AddAsync(Product product)
{
await _dbContext.Products.AddAsync(product);
await _dbContext.SaveChangesAsync();
}
public async Task UpdateAsync(Product product)
{
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
}
}
// YourProject.Infrastructure/Data/ApplicationDbContext.cs
// public class ApplicationDbContext : DbContext
// {
// public DbSet<Product> Products { get; set; }
// // ... конструкторы и конфигурация
// }
Содержит контроллеры API.
// YourProject.Api/Controllers/ProductsController.cs
// [ApiController]
// [Route("api/[controller]")]
// public class ProductsController : ControllerBase
// {
// private readonly ProductService _productService; // Или IProductService
//
// public ProductsController(ProductService productService)
// {
// _productService = productService;
// }
//
// [HttpGet("{id}")]
// public async Task<IActionResult> GetProduct(Guid id)
// {
// var productDto = await _productService.GetProductAsync(id);
// if (productDto == null) return NotFound();
// return Ok(productDto);
// }
//
// [HttpPost]
// public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
// {
// // Валидация command
// await _productService.CreateProductAsync(command.Name, command.Price);
// // Можно вернуть CreatedAtRoute или другой соответствующий результат
// return Ok();
// }
// }
//
// public class CreateProductCommand
// {
// public string Name { get; set; }
// public decimal Price { get; set; }
// }
Эти примеры иллюстрируют разделение ответственностей и направление зависимостей в луковой архитектуре. В реальных проектах также используется контейнер внедрения зависимостей (Dependency Injection Container) для связывания интерфейсов с их реализациями, обычно настраиваемый в самом внешнем слое (Api или Web).
Для наглядности, представим основные слои луковой архитектуры, их ключевые компоненты и обязанности в виде таблицы:
Слой | Основные Компоненты | Ответственность | Зависит от |
---|---|---|---|
Доменный Слой (Domain) | Сущности, Объекты-значения, Доменные сервисы, Интерфейсы репозиториев, Бизнес-правила | Определение и реализация основной бизнес-логики и моделей данных приложения. Независимость от технологий. | Ни от кого (самый внутренний слой) |
Прикладной Слой (Application) | Прикладные сервисы, DTO, Сценарии использования (use cases), Интерфейсы прикладных сервисов, Команды/Запросы | Оркестрация выполнения бизнес-сценариев, координация взаимодействия между доменными объектами и инфраструктурой через интерфейсы. | Доменный Слой |
Инфраструктурный Слой (Infrastructure) | Реализации репозиториев (работа с БД), Клиенты внешних сервисов, Логгеры, Провайдеры кэша, Работа с файлами | Реализация технических деталей: доступ к данным, интеграция с внешними системами, логирование, кэширование. | Интерфейсы Доменного и Прикладного слоев |
Слой Представления (Presentation/UI) | API контроллеры, MVC контроллеры, Веб-страницы (Views), Консольные команды, Мобильные UI компоненты | Взаимодействие с пользователем или внешними системами (клиентами). Отображение данных и прием пользовательского ввода. | Интерфейсы Прикладного слоя (иногда Инфраструктурный для DI) |
Для более глубокого понимания принципов и практического применения луковой архитектуры, рекомендуем посмотреть следующее видео. В нем доступно объясняются основные концепции, слои, их изоляция и взаимодействие, а также преимущества данного подхода.
В этом видео рассматривается слоистая архитектура, включая луковую (onion) архитектуру, ее слои, принципы изоляции, внедрение зависимостей (DI) и связь с SOLID.