Azure + Hexagonal Architecture. Гексагональна архітектура, «порти та адаптери»
Гексагональна архітектура, також відома як «порти та адаптери», — це архітектурний шаблон програмного забезпечення, що має на меті створення слабкозв’язаних компонентів додатку, які легко тестувати, підтримувати та розвивати.
Ключова концепція
Основна ідея — це відокремлення постійних частин (бізнес-логіка) від змінних (інтеграція із зовнішніми системами). Ядро бізнес-логіки додатку ізолюється від зовнішніх залежностей, таких як бази даних, користувацькі інтерфейси, сторонні API та інші сервіси. Це забезпечує мінімальну зв’язність та мінімальну залежність від технологій, що, своєю чергою, гарантує легке тестування та гнучкість системи.
Уявіть собі шестикутник, у центрі якого знаходиться ядро вашого додатку. Кожна сторона шестикутника є «портом» для взаємодії із зовнішнім світом. «Адаптери» підключаються до цих портів та реалізують конкретну технологію.
Основні компоненти
- Ядро додатку (Application Core): Це серце вашої системи. Тут міститься вся бізнес-логіка, доменні моделі та правила. Ядро не залежить від жодних зовнішніх технологій чи фреймворків. Воно визначає «порти», які йому необхідні для роботи.
- Порти (Ports): Простими словами, порти — це інтерфейси із системою, які визначають її функціонал. Вони оголошуються в ядрі додатку та описують контракт взаємодії, не містячи жодної конкретної реалізації.
- Вхідні порти (Driving Ports): Визначають, як зовнішні актори (користувачі, інші системи) можуть викликати бізнес-логіку. По суті, це API ядра вашого додатку.
- Вихідні порти (Driven Ports): Визначають, що потрібно ядру від зовнішнього світу (наприклад, отримання даних із бази даних, надсилання email).
- Адаптери (Adapters): Це реалізація інтерфейсів (портів). Важливо розуміти, що це ширше поняття, ніж просте успадкування класу від інтерфейсу. Адаптер — це цілий механізм, який «адаптує» специфічну технологію до універсального порту ядра.
- Вхідні адаптери (Driving Adapters): «Керують» додатком. Це можуть бути контролери в ASP.NET Core Web API, обробники повідомлень із черги Azure Service Bus або консольні команди.
- Вихідні адаптери (Driven Adapters): «Керуються» додатком. Кожен такий адаптер, як правило, виконує одну функцію. Типи адаптерів: запис у базу, робота з веб-запитами, робота з подіями-повідомленнями.
Ключове правило: залежності завжди спрямовані всередину, до ядра додатку. Це досягається за рахунок інверсії залежностей (Dependency Inversion Principle).
Приклад на .NET Core
Розглянемо простий приклад системи замовлень.
1. Структура проєкту
Ваш розв’язок (.sln) може мати такий вигляд:
- Ordering.sln - src - Ordering.Domain (Ядро: моделі, вихідні порти) - Ordering.Application (Ядро: логіка, вхідні порти) - Ordering.Infrastructure (Вихідні адаптери) - Ordering.Api (Вхідні адаптери)
2. Ядро додатку
Ordering.Domain/Ports/IOrderRepository.cs (Вихідний порт)
namespace Ordering.Domain.Ports;
// Цей порт визначає, який функціонал потрібен ядру від сховища даних.
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task AddAsync(Order order);
}
Ordering.Application/Ports/ICreateOrderUseCase.cs (Вхідний порт)
namespace Ordering.Application.Ports;
// Цей порт визначає, як зовнішній світ може ініціювати створення замовлення.
public interface ICreateOrderUseCase
{
Task<Guid> ExecuteAsync(CreateOrderCommand command);
}
Ordering.Application/UseCases/CreateOrderUseCase.cs (Реалізація логіки)
namespace Ordering.Application.UseCases;
public class CreateOrderUseCase : ICreateOrderUseCase
{
private readonly IOrderRepository _orderRepository;
// Залежить тільки від порту (інтерфейсу), а не від конкретної реалізації.
public CreateOrderUseCase(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<Guid> ExecuteAsync(CreateOrderCommand command)
{
var order = new Order(command.CustomerId, command.Product);
await _orderRepository.AddAsync(order);
return order.Id;
}
}
3. Адаптери
Ordering.Infrastructure/Adapters/OrderRepository.cs (Вихідний адаптер)
namespace Ordering.Infrastructure.Adapters;
// Адаптер для роботи з базою даних через EF Core.
public class OrderRepository : IOrderRepository
{
private readonly OrderingDbContext _context;
public OrderRepository(OrderingDbContext context)
{
_context = context;
}
// ... реалізація методів інтерфейсу IOrderRepository ...
}
Ordering.Api/Controllers/OrdersController.cs (Вхідний адаптер)
namespace Ordering.Api.Controllers;
// Адаптер для роботи з веб-запитами.
[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
private readonly ICreateOrderUseCase _createOrderUseCase;
public OrdersController(ICreateOrderUseCase createOrderUseCase)
{
_createOrderUseCase = createOrderUseCase;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
var command = new CreateOrderCommand(request.CustomerId, request.Product);
var orderId = await _createOrderUseCase.ExecuteAsync(command);
return CreatedAtAction(nameof(GetOrder), new { id = orderId }, null);
}
}
Застосування в Azure
Гексагональна архітектура ідеально підходить для хмарних середовищ, оскільки дозволяє легко замінювати адаптери для різних сервісів Azure.
- База даних: Ваш вихідний адаптер може працювати з Azure SQL Database або Cosmos DB. Треба змінити базу? Ок, не проблема. Ви просто створюєте новий клас-адаптер, що реалізує
IOrderRepository, і змінюєте один рядок у конфігурації залежностей. Ядро додатку не змінюється. - Повідомлення та події: Замість виклику через API, можна створити замовлення через повідомлення в Azure Service Bus. Вхідним адаптером буде не контролер, а Azure Function, яка «слухає» чергу та викликає той самий
ICreateOrderUseCase.
Плюси та мінуси
Переваги
- Тестованість: Ви можете легко тестувати ядро бізнес-логіки в ізоляції, підміняючи адаптери моками (заглушками).
- Гнучкість: Легко замінювати технології (бази даних, брокери повідомлень, UI).
- Незалежність від UI: Бізнес-логіка не прив’язана до Web API. Її можна викликати з будь-чого.
- Спроводжуваність: Чітке відокремлення бізнес-логіки та адаптерів спрощує розуміння та модифікацію коду.
Мінуси
- Надлишкова складність: Для маленьких проєктів або простих CRUD-сервісів такий підхід може бути занадто складним. Він додає додатковий шар абстракцій (інтерфейси портів), який не завжди є виправданим.
- Збільшення кількості коду: Потрібно писати більше коду (інтерфейси, класи адаптерів, мапінг даних), що може сповільнити початкову розробку.

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