Цибулева архітектура (Onion Architecture): Посібник зі створення гнучких і тестованих застосунків на .NET та Azure

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

У світі сучасної розробки програмного забезпечення створення систем, які легко підтримувати, масштабувати та тестувати, є ключовим фактором успіху. Цибулева архітектура (Onion Architecture) — це один з архітектурних підходів, що допомагає досягти цих цілей шляхом чіткого розподілу відповідальності між компонентами системи.

Основна ідея

Уявіть собі цибулину. Вона має ядро та кілька шарів, що його оточують. Кожен наступний шар може взаємодіяти лише з тим, що знаходиться безпосередньо під ним, але не навпаки. Внутрішні шари нічого не знають про зовнішні.

Onion Architecture | blog.allegro.tech

Цей самий принцип застосовується і в програмуванні:

  • Ядро (Domain Model & Domain Services): Самий центр вашої системи. Тут знаходяться бізнес-сутності та бізнес-логіка, які не залежать від жодних технологій. Це те, що робить ваш застосунок.
  • Шар застосунку (Application Services): Організовує роботу доменної моделі. Він містить сценарії використання (use cases) та координує взаємодію між ядром і зовнішнім світом. Цей шар відповідає на питання, як застосунок виконує свої функції.
  • Шар інфраструктури (Infrastructure): Зовнішній шар, що містить усі технічні деталі: бази даних, веб-фреймворки, сторонні API, файлові системи тощо.

Головне правило — залежності спрямовані всередину. Шар інфраструктури залежить від шару застосунку, а шар застосунку — від доменного ядра. Ядро не залежить ні від чого.

Ізоляція ядра

Ключова перевага цибулевої архітектури — повна ізоляція доменної логіки. Ваша бізнес-логіка не повинна залежати від того, яку базу даних ви використовуєте (PostgreSQL чи MS SQL), як ви відображаєте інтерфейс (веб-сторінка чи мобільний додаток) і звідки отримуєте дані (API чи локальний файл).

Ця ізоляція досягається за рахунок інверсії залежностей (Dependency Inversion Principle). Внутрішні шари визначають інтерфейси (контракти), а зовнішні шари надають їхні реалізації.

Розгляньмо простий приклад: система для керування замовленнями.

1. Ядро (Domain)

Це наш центр. Тут немає посилань на Entity Framework, ASP.NET Core чи Azure. Лише чистий C#.

Проєкт: Orders.Domain

// Сутність замовлення
public class Order
{
    public Guid Id { get; private set; }
    public decimal TotalPrice { get; private set; }
    public DateTime CreatedDate { get; private set; }
    public bool IsShipped { get; private set; }
    public Order(decimal totalPrice)
    {
        Id = Guid.NewGuid();
        TotalPrice = totalPrice > 0 ? totalPrice : throw new ArgumentException("Ціна повинна бути позитивною.");
        CreatedDate = DateTime.UtcNow;
        IsShipped = false;
    }
    public void Ship()
    {
        if (IsShipped)
        {
            throw new InvalidOperationException("Замовлення вже відправлено.");
        }
        IsShipped = true;
    }
}
// Інтерфейс для репозиторію (абстракція від бази даних)
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
    Task AddAsync(Order order);
    Task SaveChangesAsync();
}

Тут Order — це доменна модель з бізнес-правилами (ціна має бути позитивною, не можна відправити вже відправлене замовлення). IOrderRepository — це контракт, який каже: «Мені потрібен хтось, хто вміє зберігати та отримувати замовлення». Ядру байдуже, хто і як це робитиме.

2. Шар застосунку (Application)

Цей шар використовує ядро для виконання конкретних завдань.

Проєкт: Orders.Application (залежить від Orders.Domain)

// Інтерфейс для сповіщень (абстракція від email, SMS тощо)
public interface INotificationService
{
    Task SendOrderCreatedNotificationAsync(Guid orderId);
}
// Сервіс для керування замовленнями
public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly INotificationService _notificationService;
    public OrderService(IOrderRepository orderRepository, INotificationService notificationService)
    {
        _orderRepository = orderRepository;
        _notificationService = notificationService;
    }
    public async Task<Guid> CreateOrderAsync(decimal price)
    {
        var order = new Order(price);
        
        await _orderRepository.AddAsync(order);
        await _orderRepository.SaveChangesAsync();
        
        await _notificationService.SendOrderCreatedNotificationAsync(order.Id);
        
        return order.Id;
    }
}

OrderService реалізує сценарій створення замовлення. Він використовує абстракції (IOrderRepository, INotificationService) та координує їхню роботу, але не знає деталей їхньої реалізації.

3. Шар інфраструктури (Infrastructure)

Це зовнішній шар, де ми реалізуємо наші інтерфейси за допомогою конкретних технологій.

Проєкт: Orders.Infrastructure (залежить від Orders.Application)

// Реалізація репозиторію за допомогою Entity Framework Core
public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;
    public OrderRepository(ApplicationDbContext context)
    {
        _context = context;
    }
    public async Task<Order?> GetByIdAsync(Guid id)
    {
        return await _context.Orders.FindAsync(id);
    }
    public async Task AddAsync(Order order)
    {
        await _context.Orders.AddAsync(order);
    }
    public async Task SaveChangesAsync()
    {
        await _context.SaveChangesAsync();
    }
}

4. Точка входу (Web API)

Наш веб-контролер, який пов’язує все разом.

Проєкт: Orders.Api (залежить від Orders.Infrastructure та Orders.Application)

[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;
    public OrdersController(OrderService orderService)
    {
        _orderService = orderService;
    }
    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderRequest request)
    {
        var orderId = await _orderService.CreateOrderAsync(request.Price);
        return Ok(new { OrderId = orderId });
    }
}

У Program.cs ми налаштовуємо впровадження залежностей (Dependency Injection), щоб зв’язати інтерфейси з їхніми реалізаціями.

Інтеграція з Azure

Краса цибулевої архітектури в тому, що для інтеграції з хмарними сервісами нам потрібно лише додати нові реалізації в шарі інфраструктури, не чіпаючи ядро та логіку застосунку.

  • База даних: Замість локального SQL Server ми можемо використовувати Azure SQL Database або Cosmos DB. Для цього достатньо змінити рядок підключення та, можливо, реалізацію репозиторію (IOrderRepository), якщо ми переходимо з реляційної бази на NoSQL.
  • Сповіщення: Інтерфейс INotificationService можна реалізувати за допомогою Azure Service Bus для відправки повідомлень у чергу або Azure Communication Services для надсилання Email/SMS.

Приклад реалізації INotificationService за допомогою Azure Service Bus:

// У проєкті Orders.Infrastructure
public class ServiceBusNotificationService : INotificationService
{
    private readonly ServiceBusClient _client;
    private const string QueueName = "order-notifications";
    public ServiceBusNotificationService(string connectionString)
    {
        _client = new ServiceBusClient(connectionString);
    }
    public async Task SendOrderCreatedNotificationAsync(Guid orderId)
    {
        ServiceBusSender sender = _client.CreateSender(QueueName);
        var messageBody = JsonSerializer.Serialize(new { OrderId = orderId });
        var message = new ServiceBusMessage(messageBody);
        await sender.SendMessageAsync(message);
    }
}

Тепер у файлі конфігурації Program.cs ми просто замінюємо стару реалізацію на нову. Бізнес-логіка в OrderService залишається незмінною!

Чіткий розподіл та документація

  • Структура проєктів у рішенні: Кожен шар — це окремий проєкт (.csproj). Це фізично розділяє код та контролює залежності.
  • Тестованість: Оскільки доменна логіка не залежить від зовнішніх систем, її легко тестувати за допомогою юніт-тестів. Сервіси застосунку можна тестувати, підміняючи репозиторії та інші залежності на «заглушки» (mocks).
  • Самодокументований код: Така структура сама по собі є гарною документацією. Хочете зрозуміти бізнес-правила? Відкрийте проєкт Domain. Хочете дізнатися, як ми працюємо з базою даних? Зазирніть в Infrastructure.

Цибулева архітектура — це не просто набір правил, а філософія, що ставить бізнес-логіку в центр всесвіту вашого застосунку. Це інвестиція в майбутнє проєкту, яка окупається за рахунок гнучкості, простоти підтримки та високої швидкості розробки в довгостроковій перспективі.

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Тема цікава, а от стаття на рівні студентського реферату. Зрозуміло, що для примітивного CRUD сервісу реалізація буде очевидною. Але архітектура — і тим більше доменна логіка, мають сенс тільки у великій ентерпрайз системі.
Показати цю техніку на примітивних прикладах — це як навчитися жонглювати двома кульками. Не дуже важко опанувати і з невеликим досвідом можна підкидати дві кульки дуже спритно. От тільки якщо додати третю кульку — усе посиплеться.
Наприклад перше питання:
а де у цій архітектурі саме бізнес-логіка? Не CRUD, а обчислення, у яких можуть бути залучені десятки доменних сутностей.
Проблеми одночасної роботи — адже сотні юзерів будуть одночасно змінювати сотні сутностей і як забезпечити консистентність?
Проблема асинхронності — якщо усе іде через черги, то скільки має чекати UI? Чи має він постійно слухати сервер? Як одна сутність знає що інша сутність змінилася і щось треба перерахувати?
Якщо вже розбирати архітектуру великої системи — то краще малювати якісь схеми, а не код інтерфейсів. Бо така архітектура має вирішувати абстракції вищого рівня. Наприклад взаємодію мікросервісів.

Як що відверто, Clean Architecture та цибуля, однакови по суті.

Почитайте про Clean Architecture та не позортеся з цією цибулею будь ласка.

буде про

Clean Architecture

також

Виглядає чисто як нічим не вмотивоване бажання образити шановного пана автора статті, без жодних пояснень чи прикладів того як має бути та без жодної аргументації.

На відміну від інфоциганської маячні Мартіна, те, що в статті, хоча б наводить на розумні думки.

Підписатись на коментарі