Чиста Архітектура (Clean Architecture) в .NET та Azure: від теорії до практики
Чиста архітектура — це не просто набір правил, а філософія проєктування програмного забезпечення, запропонована Робертом Мартіном (Uncle Bob). Її головна мета — створення систем з високою адаптивністю, які легко підтримувати, тестувати та масштабувати. В основі цієї філософії лежить розділення відповідальностей, незалежність від фреймворків та суворий контроль залежностей.
Розгляньмо її ключові принципи та подивімося, як їх реалізувати за допомогою .NET та хмарних сервісів Azure.
Ключовий Принцип: Правило Залежностей
Основою Чистої архітектури є правило залежностей. Його можна уявити у вигляді концентричних кіл або шарів.
- Головне правило: Залежності можуть бути спрямовані лише ззовні всередину. Код у внутрішньому шарі нічого не знає про код у зовнішньому.
- Взаємодія: Об’єкти у зовнішніх шарах взаємодіють із внутрішніми через абстракції (інтерфейси), які визначені у внутрішніх шарах. Це називається інверсією залежностей.
Такий підхід забезпечує стабільність ядра: зміни в базі даних, фреймворку або інтерфейсі користувача не впливають на бізнес-логіку.
Шари Чистої Архітектури на прикладі .NET
Розглянемо типову структуру проєкту на .NET, що дотримується принципів Clean Architecture.
1. Ядро (Core): Domain + Application
Це серце вашої системи. Воно не залежить від жодних зовнішніх технологій.
Domain Layer (Сутності)
Це найглибше коло. Тут містяться фундаментальні бізнес-об’єкти (Entities) та критично важливі бізнес-правила, які не залежать від конкретного застосунку.
- Що тут: Бізнес-сутності та їхня логіка.
- Приклад (.NET): Сутність
Orderзі списком товарів (OrderLine) та інкапсульованою бізнес-логікою.
// Проєкт: Core.Domain
// Немає залежностей від зовнішніх технологій
public class OrderId
{
public Guid Value { get; }
public OrderId(Guid value) => Value = value;
}
public class Order
{
private readonly List<OrderLine> _lines = new();
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
public decimal TotalAmount => _lines.Sum(l => l.TotalPrice);
public OrderStatus Status { get; private set; }
// Приватний конструктор для контролю створення через фабричний метод
private Order(CustomerId customerId)
{
Id = new OrderId(Guid.NewGuid());
CustomerId = customerId;
Status = OrderStatus.Pending;
}
// Фабричний метод для створення замовлення, що гарантує валідність
public static Order Create(CustomerId customerId)
{
if (customerId == null)
{
throw new ArgumentNullException(nameof(customerId), "Customer ID cannot be null.");
}
return new Order(customerId);
}
// Метод, що інкапсулює бізнес-логіку додавання товару
public void AddLine(ProductId productId, int quantity, decimal price)
{
if (Status != OrderStatus.Pending)
{
throw new InvalidOperationException("Cannot add items to an order that is not pending.");
}
var existingLine = _lines.FirstOrDefault(l => l.ProductId == productId);
if (existingLine != null)
{
existingLine.IncreaseQuantity(quantity);
}
else
{
_lines.Add(new OrderLine(Id, productId, quantity, price));
}
}
public void Confirm()
{
if (Status != OrderStatus.Pending) throw new InvalidOperationException("Order is not pending.");
Status = OrderStatus.Confirmed;
// Тут можна було б додати доменну подію OrderConfirmedEvent
}
}
public class OrderLine
{
public ProductId ProductId { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
public decimal TotalPrice => Quantity * UnitPrice;
// ... конструктор та методи
}
Application Layer (Сценарії використання)
Цей шар містить Use Cases (сценарії використання) — логіку, специфічну для вашого застосунку. Він керує роботою з доменними сутностями для виконання конкретних завдань.
- Що тут: Сервіси застосунку, обробники команд та запитів (CQRS), інтерфейси для зовнішніх залежностей (наприклад,
IOrderRepository). - Приклад (.NET): Сценарій створення замовлення, який залежить від кількох абстракцій (репозиторіїв) та оркеструє доменну модель.
// Проєкт: Core.Application
// Залежить лише від Core.Domain
// Команда, що містить усі необхідні дані для створення замовлення
public record CreateOrderCommand(Guid CustomerId, List<OrderItemDto> Items) : IRequest<Guid>;
public record OrderItemDto(Guid ProductId, int Quantity);
// Обробник, що використовує доменну модель та репозиторії
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _orderRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IProductRepository _productRepository;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IProductRepository productRepository)
{
_orderRepository = orderRepository;
_customerRepository = customerRepository;
_productRepository = productRepository;
}
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var customerId = new CustomerId(request.CustomerId);
var customer = await _customerRepository.GetByIdAsync(customerId);
if (customer == null)
{
throw new ApplicationException("Customer not found.");
}
var order = Order.Create(customerId);
foreach (var item in request.Items)
{
var productId = new ProductId(item.ProductId);
var product = await _productRepository.GetByIdAsync(productId);
if (product == null)
{
throw new ApplicationException($"Product with ID {item.ProductId} not found.");
}
// Логіка перевірки наявності товару на складі...
order.AddLine(productId, item.Quantity, product.Price);
}
await _orderRepository.AddAsync(order);
return order.Id.Value;
}
}
2. Зовнішні Шари: Infrastructure + Presentation
Ці шари містять конкретні реалізації та технології. Вони є «деталями», які можна легко замінити.
Interface Adapters / Infrastructure (Адаптери та Інфраструктура)
Тут відбувається конвертація даних з формату, зручного для зовнішнього світу (наприклад, HTTP JSON), у формат, зручний для ядра (команди, доменні об’єкти). Цей шар реалізує інтерфейси, визначені в Application Layer.
- Що тут: Реалізації репозиторіїв (наприклад, з використанням Entity Framework Core), клієнти для зовнішніх API, брокери повідомлень (наприклад, Azure Service Bus).
- Приклад (.NET + Azure): Реалізація репозиторію з використанням EF Core, яка працює з Azure SQL Database та завантажує пов’язані дані.
// Проєкт: Infrastructure
// Залежить від Core.Application та реалізує його інтерфейси
public class OrderRepository : IOrderRepository
{
private readonly SalesDbContext _context;
public OrderRepository(SalesDbContext context)
{
_context = context;
}
public async Task<Order> GetByIdAsync(OrderId id)
{
// Включаємо пов'язані дані, щоб завантажити агрегат повністю
return await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
}
public async Task AddAsync(Order order)
{
await _context.Orders.AddAsync(order);
// Збереження змін зазвичай відбувається в Unit of Work
}
}
Frameworks & Drivers (Драйвери та Фреймворки)
Зовнішній шар. Тут містяться конкретні технології, які запускають застосунок та взаємодіють з користувачем або іншими системами.
- Що тут: ASP.NET Core Web API, MVC, gRPC, консольні застосунки, Azure Functions, UI-фреймворки (React, Angular).
- Приклад (.NET): Контролер в ASP.NET Core, який приймає складну модель DTO та повертає коректну відповідь (201 Created).
// Проєкт: Presentation (або WebApi)
// Залежить від Core.Application та використовує MediatR для відправки команд
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly ISender _sender; // Використовуємо ISender з MediatR
public OrdersController(ISender sender)
{
_sender = sender;
}
[HttpPost]
[ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
// Мапування DTO з запиту на команду Application Layer
var command = new CreateOrderCommand(request.CustomerId, request.Items);
var orderId = await _sender.Send(command);
// Повертаємо 201 Created з посиланням на новий ресурс
return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetOrder(Guid id)
{
// Тут буде логіка для запиту (Query) замовлення...
return Ok($"Fetching order with ID {id}");
}
}
// DTO для запиту, що приходить ззовні
public class CreateOrderRequest
{
public Guid CustomerId { get; set; }
public List<OrderItemDto> Items { get; set; }
}
Переваги Чистої Архітектури
- Незалежність від фреймворку: Ваша бізнес-логіка не прив’язана до ASP.NET, EF Core або будь-якого іншого фреймворку. Це дозволяє оновлювати або змінювати технології без переписування ядра.
- Тестованість: Ядро системи можна тестувати ізольовано, без необхідності запускати вебсервер або базу даних. Це робить тести швидкими та надійними.
- Масштабованість: Чітке розділення на шари ідеально підходить для мікросервісної архітектури. Ядро можна «обгорнути» в різні «драйвери»: API-контролер, обробник повідомлення з черги Azure Service Bus або тригер Azure Function.
- Легке додавання функцій: Нові сценарії використання додаються в Application Layer, не зачіпаючи наявні. Інтеграція з новою зовнішньою системою — це просто додавання нового адаптера в шар Infrastructure.
Висновок
Чиста архітектура вимагає дисципліни та початкових витрат на проєктування, але окупається в довгостроковій перспективі. Вона дозволяє створювати надійні, гнучкі та довговічні системи, які легко адаптуються до мінливих вимог бізнесу та технологій. Для складних проєктів, розгорнутих у хмарному середовищі на кшталт Azure, такий підхід стає не просто хорошою практикою, а необхідністю для успішного розвитку.

1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів