Деякі міркування про чисту архітектуру, і чи варто слідувати догмам
Примітка
Цей текст не є претензією на остаточну істину. Це радше практичні міркування розробника, який багато років працює з бізнес-системами, базами даних, високим навантаженням і реальними виробничими помилками.
Стаття велика, її буде важко читати, а мені було важко її писати. Складність в тому, що треба було співставити теорію, про яку написано багато, з практикою, про яку пишуть мало. По перше — практика, це про конкретне, і конкретних прикладів безліч, описати всі приклади майже неможливо. По друге, тим людям, які щодня пишуть код для великих проектів, важко знайти час для написання статей. Терміни тиснуть, замовники питають «коли», і ти цю статтю пишеш місяць або два уривками. Втрачаєш думки, перечитуєш, переписуєш, потім бачиш, що з’явились повтори, і треба все взяти й переписати наново. Але ж терміни тиснуть, замовники питають «коли»... І так по колу. Тому вирішив викласти те що є, і як воно є. Можливо хтось продереться через хащі думок і зауважень, і знайде для себе щось корисне.
Про Clean Architecture написано багато статей, книжок і доповідей. Часто її подають як майже універсальний підхід до побудови програмних систем: бізнес-логіка не залежить від фреймворків, код легше тестувати, систему простіше розвивати й масштабувати.
Ці аргументи мають сенс. Проблема починається тоді, коли архітектурний підхід перетворюється на догму, а не на інженерний інструмент. Тоді замість яснішої структури можна отримати зайві абстракції, розмиту відповідальність, складніший код і проблеми зі швидкодією.
Для прикладу розглянемо статтю «Чиста Архітектура (Clean Architecture) в .NET та Azure: від теорії до практики». Її цінність у тому, що вона коротко демонструє типову структуру Clean Architecture на прикладі .NET: Domain, Application, Infrastructure та Presentation. Саме на таких компактних прикладах добре видно як переваги підходу, так і ризики його практичного застосування.
Що зазвичай називають перевагами Clean Architecture
Автор згаданої статті перелічує кілька типових переваг чистої архітектури:
- Незалежність від фреймворку. Бізнес-логіка не прив’язана безпосередньо до ASP.NET, EF Core або іншої зовнішньої технології.
- Тестованість. Ядро системи можна тестувати ізольовано, без запуску вебсервера або реальної бази даних.
- Масштабованість. Система розділена на шари, а ядро можна використовувати через різні зовнішні механізми: API-контролер, обробник повідомлень, Azure Function тощо.
- Легше додавання функцій. Нові сценарії використання додаються в Application Layer, а інтеграції з зовнішніми системами — через адаптери в Infrastructure.
Усе це справді може бути корисним. Але в реальному проєкті важливо не лише розділити код на шари. Потрібно також зберегти цілісність бізнес-логіки, прозорість коду, контроль над транзакціями, ефективність запитів до бази даних і зрозумілу відповідальність кожного класу.
Практичне питання № 1: де живе бізнес-правило
У прикладі з Domain Layer сутність Order створюється через фабричний метод:
public static Order Create(CustomerId customerId)
{
if (customerId == null)
{
throw new ArgumentNullException(nameof(customerId), "Customer ID cannot be null.");
}
return new Order(customerId);
}
На перший погляд усе коректно: об’єкт Order не можна створити без CustomerId. Але виникає практичне питання: чи достатньо цього для реальної бізнес-системи?
Наявність об’єкта CustomerId ще не означає, що в базі даних справді існує покупець із таким ідентифікатором. Особливо якщо запит приходить через зовнішнє API, де клієнт може передати будь-який Guid.
У самій сутності Order цю перевірку виконати складно, і в межах Clean Architecture це очікувано: доменна сутність не повинна напряму звертатися до бази даних або репозиторію. Тому в прикладі перевірка існування покупця переноситься в CreateOrderCommandHandler:
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);
Це помилка чи нормальний розподіл відповідальності?
Тут важливо розділити два типи правил.
Локальні інваріанти сутності мають залишатися всередині Order. Наприклад: не можна додавати рядки до підтвердженого замовлення, кількість товару має бути додатною, ціна не повинна бути від’ємною, повторний товар у замовленні потрібно або об’єднувати, або явно забороняти.
Правила, які потребують зовнішніх даних, зазвичай живуть на рівні сценарію використання або доменного сервісу. Наприклад: чи існує покупець, чи існує товар, чи доступна потрібна кількість товару на складі, яку актуальну ціну потрібно застосувати, в якій транзакції виконувати операцію.
Тому сама по собі перевірка покупця в CreateOrderCommandHandler не є помилкою. Але є ризик: якщо такі перевірки розкидані по різних обробниках, а сама доменна модель залишається надто «тонкою», то бізнес-логіка поступово розмивається між Application Layer, репозиторіями, сервісами й контролерами. У результаті система формально має чисту архітектуру, але її правила стає важче бачити й контролювати.
Практичне питання № 2: тестування не повинно підміняти реальну надійність
Один із головних аргументів на користь Clean Architecture — простота unit-тестування. Це справді перевага: локальні бізнес-правила сутності Order можна перевірити швидко й ізольовано.
Наприклад, unit-тест добре перевіряє, що не можна додати товар до замовлення, яке вже підтверджене. Або що повторний товар у замовленні збільшує кількість, якщо саме така поведінка передбачена бізнес-логікою.
Але unit-тести не замінюють інтеграційні тести. Значна частина складних помилок у бізнес-системах виникає не в окремому методі, а на стику кількох компонентів: база даних, транзакції, блокування, паралельний доступ, конкурентне оновлення залишків, помилки збереження, реальні обмеження SQL-сервера.
Наприклад, дві видаткові накладні можуть одночасно списувати одні й ті самі товарні залишки. Якщо вони блокують товари в різному порядку, може виникнути deadlock. Такий сценарій майже неможливо адекватно перевірити простим unit-тестом із mock-репозиторієм. Для цього потрібні інтеграційні, навантажувальні або спеціально змодельовані конкурентні тести.
Отже, ізольовані тести корисні, але вони перевіряють лише частину системи. Якщо найризикованіші ділянки пов’язані з базою даних і транзакціями, то саме ці ділянки мають бути покриті тестами відповідного рівня.
Практичне питання № 3: незалежність від фреймворку має свою ціну
Clean Architecture прагне відділити бізнес-логіку від конкретних фреймворків. Це логічна мета, особливо для довготривалих проєктів. Проте повна ізоляція від інструментів доступу до даних не завжди безкоштовна.
У реальних бізнес-системах база даних — це не другорядна деталь. Вона зберігає стан системи, забезпечує цілісність, виконує складні запити, контролює транзакції й обмеження. EF Core, якщо ним користуватися обережно, дає багато можливостей для оптимізації: проєкції, Include, фільтрацію на сервері, batch-запити, транзакції, контроль tracking/no-tracking, перевірку SQL через ToQueryString().
Якщо заради «чистоти» повністю сховати EF Core за надто загальними репозиторіями, можна втратити частину цих можливостей. У такому випадку код може стати формально правильним з погляду шарів, але менш зрозумілим і менш ефективним на практиці.
Практичне питання № 4: продуктивність і проблема N+1 запитів
Найбільш показовий фрагмент у прикладі — обробка товарів у циклі:
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);
}
Тут кожен товар завантажується окремим запитом. Для невеликого навчального прикладу це прийнятно. Але в реальній системі такий підхід швидко створює проблему N+1 запитів.
Якщо в документі 10 позицій, різниця може бути непомітною. Якщо в документі тисячі або десятки тисяч позицій, окремий запит на кожен товар перетворюється на серйозне навантаження на SQL-сервер і суттєво збільшує час виконання операції.
Важливо уточнити: це не обов’язкова проблема Clean Architecture як концепції. Це проблема конкретної реалізації сценарію використання. Clean Architecture не забороняє завантажити всі потрібні товари одним запитом. Але якщо архітектурні обмеження або надто загальні репозиторії підштовхують розробника до поштучного завантаження, це вже практичний сигнал, що абстракції почали заважати.
Більш практичний варіант виглядав би приблизно так:
var productIds = request.Items .Select(item => new ProductId(item.ProductId)) .Distinct() .ToList(); var products = await _productRepository.GetByIdsAsync(productIds, cancellationToken);
Після цього можна перевірити, чи всі товари знайдені, побудувати словник за ідентифікатором і вже тоді формувати замовлення. Це дозволяє зменшити кількість звернень до бази даних і краще контролювати продуктивність.
Що варто покращити в такому прикладі
Щоб приклад був ближчим до реальної практики, я б звернув увагу на такі моменти:
1. Додати явну валідацію value object
CustomerId, ProductId і OrderId можуть перевіряти, що значення не дорівнює Guid.Empty. Це проста, але корисна валідація на рівні доменної моделі.
2. Не змішувати локальні інваріанти й зовнішні перевірки
Order має відповідати за власну внутрішню коректність. А перевірки, які потребують бази даних або інших агрегатів, мають бути явно оформлені на рівні application service, command handler або domain service.
3. Уникати поштучного завантаження даних
Якщо сценарій працює з колекцією товарів, репозиторій або query service має підтримувати batch-завантаження. Інакше архітектура буде виглядати правильно лише на маленьких прикладах.
4. Не ховати складні запити за надто загальними інтерфейсами
Іноді краще мати спеціалізований метод для конкретного сценарію, ніж універсальний репозиторій, який формально красивий, але не дозволяє оптимально отримати дані.
5. Тестувати не лише класи, а й ризикові сценарії
Unit-тести корисні для локальних правил. Але для систем із базою даних потрібно також тестувати транзакції, конкурентний доступ, інтеграцію з реальною СУБД і поведінку під навантаженням.
Практики автора: приклад архітектури робочої бізнес-системи
Щоб не залишатися лише в площині критики навчального прикладу, варто коротко описати інший підхід — не як універсальний рецепт, а як приклад практичної реалізації довгоживучого високонавантаженого проєкту.
Йдеться про Trade Control Utility — систему обліку товарів, документів, залишків, закупівельних партій, переміщень, продажів і взаємодії з іншими сервісами. Це не лабораторний приклад і не демонстраційний проєкт. Система багато років працює у виробничому середовищі, обробляє за добу сотні тисяч документів і має справу з великими базами даних, реальними користувачами, зовнішніми інтеграціями, помилками обладнання, паралельними операціями та постійною потребою зберігати логічну цілісність даних.
Шари системи
Архітектура такого проєкту також розділена на шари, але ці шари мають не догматичну, а практичну природу.
- Клієнтський UI-рівень відповідає за взаємодію з користувачем: форми, редактори, списки, відображення прогресу операцій, реакцію на події.
- DataSources-рівень на стороні клієнта інкапсулює сценарії завантаження, збереження, оновлення й додаткового завантаження даних. Він працює з каналами зв’язку, викликає API і перетворює серверну поведінку на зручну модель для UI.
- Proxy-рівень описує зовнішні контракти між клієнтом і сервером, але не зводиться лише до пасивних транспортних DTO. Класи мають суфікс
Proxy, бо вони одночасно виконують роль контрактів обміну й робочих моделей редактора на клієнті. Наприклад, вони можуть реалізовуватиINotifyPropertyChanged, перераховувати суму накладної, оновлювати залежні поля, підтримувати стан рядків, помилки введення та іншу поведінку, потрібну UI. - Серверний API-рівень приймає запити, створює користувацький контекст бази, відкриває транзакції, викликає доменні операції, публікує події та повертає результат клієнту.
- Доменний/Data-рівень містить основні сутності системи: документи, товарні рядки, складські записи, журнали руху товарів, довідники, правила проведення й відкату документів.
- Оптимізовані транспортні DTO використовуються там, де звичайний JSON або надто глибока об’єктна модель створюють зайве навантаження. Для великих обсягів даних можуть застосовуватися спеціальні плоскі DTO та MessagePack.
Такий підхід дозволяє скоротити кількість проміжних класів, які представляють одну й ту саму сутність на різних рівнях системи. Замість ланцюжка на кшталт DocumentProxy → DocumentDto → Document використовується простіша й пряміша схема DocumentProxy → Document. DocumentProxy залишається зовнішньою моделлю для клієнта та API, а Document — внутрішньою доменною сутністю з повною бізнес-поведінкою. Це компроміс між чистотою розділення й практичною простотою супроводу.
Тобто система не є безшаровою. Але головна мета шарів — не відповідати певній архітектурній схемі з книжки, а розділити відповідальність там, де це реально допомагає підтримці, розвитку, продуктивності та контролю помилок.
Свідоме використання EF Core всередині доменної моделі
У цьому підході автор не претендує на повну ізоляцію доменних сутностей від фреймворку доступу до даних. Навпаки, доменні сутності щільно інтегровані з EF Core і максимально використовують його можливості.
Наприклад, сутності отримують доступ до TcuContext, можуть завантажувати потрібні навігаційні властивості, будувати спеціалізовані IQueryable-запити, використовувати Include, ThenInclude, EF.Functions.Like, індексні підказки, транзакції, RowVersion, ToQueryString() для аналізу повільних запитів і точкове завантаження пов’язаних даних.
З погляду «ідеальної» Clean Architecture це може виглядати як залежність домену від інфраструктури. Але в реальній обліковій системі база даних — це не зовнішня випадкова деталь. Це середовище існування сутностей. Саме там зберігаються залишки, закупівельні партії, документи, зв’язки між документами, історія руху товарів, транзакційність і обмеження цілісності.
Тому тут обрано інший компроміс: не ховати EF Core за універсальним репозиторієм, який збіднює модель, а дати сутностям достатньо інструментів, щоб вони могли захищати власну цілісність і виконувати складні бізнес-операції ефективно.
Як виглядає обробка документа
Типовий сценарій роботи з документом проходить кілька рівнів.
Клієнт працює з DocumentProxy або його нащадками. Це не лише контейнер для передавання даних на сервер, а й модель, з якою працює редактор документа: вона може реагувати на зміну властивостей, перераховувати підсумки, підтримувати стан рядків і давати користувачу швидкий зворотний зв’язок ще до збереження. Під час збереження proxy-об’єкт передається на сервер. Контролер створює UserTcuContext, відкриває транзакцію й передає роботу доменній моделі. Далі Document.Update знаходить або створює потрібний документ, викликає FromProxy, а для товарних документів оновлює рядки через внутрішню логіку GoodsDocument та DocumentDetail.
Проведення документа — ще показовіший приклад. Контролер не проводить документ самостійно. Він лише створює контекст, відкриває транзакцію, завантажує документ і викликає document.Approve(). Далі сама сутність документа виконує свою життєву операцію: завантажує потрібні навігаційні властивості, перевіряє права користувача, перевіряє послідовність документів, закриті періоди, статус, валюту, підрозділ, тип операції, після чого передає проведення спеціалізованій реалізації.
Для товарного документа це означає завантаження товарних рядків, завантаження складських записів, створення пов’язаних переоцінок, проходження по рядках документа, перевірку товару, кількості, одиниці виміру, доступності продажу або приходу, підбір закупівельних партій, створення записів журналу руху товару, зміну залишків у InventoryRecord і перерахунок підсумків.
Це не набір розрізнених handler-ів, які випадково змінюють стан різних таблиць. Це послідовність взаємодії сутностей, де Document, GoodsDocument, DocumentDetail, InventoryRecord і InventoryJournalRecord разом утворюють робочу доменну систему.
Сутність не ізольована, але вона захищає себе
Такий підхід добре узгоджується з уявленням про доменну сутність як про життєздатну систему. Сутність не існує у вакуумі. Вона взаємодіє з іншими сутностями, завантажує потрібний контекст, створює підлеглі об’єкти, перевіряє стан середовища й тільки після цього змінює себе та пов’язані об’єкти.
Наприклад, товарний рядок документа під час проведення не просто зменшує число в полі Quantity. Він знаходить або створює потрібний складський запис, перевіряє товар, кількість, одиницю виміру, статус продажу або приходу, підбирає закупівельні партії, створює записи руху товару й передає зміну залишку об’єкту InventoryRecord. А InventoryRecord, у свою чергу, не є пасивним рядком таблиці: він змінює кількість, середньозважену закупівельну ціну, контролює від’ємні залишки, оновлює службові показники й бере участь у відкаті операцій.
Тобто сутності тут не «мертві». Вони мають поведінку, пам’ятають свій стан, знають свій контекст і мають механізми самозахисту. При цьому вони не є повністю ізольованими: як і живі організми, вони існують у середовищі та взаємодіють з іншими сутностями.
Чому не варто ізолюватися від EF Core за будь-яку ціну
Один із типових аргументів Clean Architecture — можливість замінити фреймворк доступу до даних. Теоретично це звучить привабливо. Практично ж для великої облікової системи, яка багато років розвивається на EF Core, імовірність повної заміни цього фреймворку майже нульова.
EF Core є відкритим, довгоживучим і активно підтримуваним інструментом. Він розвивається разом із .NET, має велику екосистему, добре документований і надає можливості, які важко без втрат сховати за надто загальними абстракціями.
Тому в такому проєкті надмірна підготовка до гіпотетичної заміни EF Core може стати не перевагою, а джерелом зайвої складності. Це близько до відомої інженерної думки про шкоду передчасної оптимізації. У цьому випадку йдеться навіть не стільки про оптимізацію швидкодії, скільки про передчасну універсалізацію архітектури: ми ускладнюємо сьогоднішній код заради майбутнього сценарію, який майже напевно ніколи не настане.
Якщо система багато років працює на конкретному технологічному стеку, а основні ризики пов’язані не із заміною ORM, а з продуктивністю, транзакціями, цілісністю залишків, конкурентним доступом і зрозумілістю бізнес-правил, то саме ці ризики й мають бути в центрі архітектурного рішення.
Доменна сутність як життєздатна система
Окремо варто підкреслити ще одну важливу думку. Доменна сутність не повинна бути просто контейнером властивостей або набором алгоритмів із приватними сетерами. У реальній бізнес-системі вона більше схожа на живу одиницю: має власні межі, внутрішній стан, правила самозахисту й водночас взаємодіє з іншими сутностями.
Така сутність не обов’язково має напряму звертатися до бази даних або знати про EF Core, SQL Server чи конкретний репозиторій. Але вона не повинна створюватися в завідомо нежиттєздатному стані. Якщо Order може бути створений для неіснуючого клієнта, то проблема не лише в технічній валідації. Проблема в тому, що базова умова існування замовлення не була гарантована на момент його народження.
У цьому сенсі доменна модель має бути не анемічною, а стійкою. Вона повинна отримувати достатньо перевіреного контексту, щоб зберігати власну цілісність. Інакше ми отримуємо не повноцінну бізнес-сутність, а лише фрагмент бізнес-логіки, який сподівається, що всі критичні умови вже перевірені десь зовні.
Це можна порівняти з виходом у море на яхті. Формально на борту можуть бути коробки з написом «продукти», але перед подорожжю важливо переконатися, що всередині справді продукти, а не цегла. Так само й замовлення: воно має бути забезпечене всім необхідним для коректного існування ще до того, як система почне працювати з ним як із валідною доменною сутністю.
Інженерне рішення майже завжди є компромісом. Якщо ми зменшуємо залежність від фреймворку, можемо заплатити складністю. Якщо максимально ізолюємо код для unit-тестів, можемо винести за межі тестування найважливіші інтеграційні ризики. Якщо створюємо абстракції над EF Core, можемо втратити частину його оптимізаційних можливостей.
Тому варто вивчати Clean Architecture, Onion Architecture, DDD, CQRS, мікросервісну архітектуру, gRPC, шини подій, різні типи тестування й сучасні AI-інструменти. Але застосовувати їх потрібно не заради самої назви, а для конкретної задачі, конкретної команди, конкретного навантаження й конкретних обмежень проєкту.
Архітектура має допомагати розробнику бачити систему ясніше, а не змушувати його боротися з власними шарами. Коли це розуміння з’являється, рішення стають спокійнішими, точнішими й набагато практичнішими.
Що дає такий підхід
Перевага такого підходу не в «чистоті» залежностей, а в практичній цілісності системи.
- Бізнес-операція виконується там, де є достатньо контексту для її коректного виконання.
- Сутності можуть захищати власний стан, а не лише приймати вже підготовлені значення ззовні.
- Складні запити можна оптимізувати засобами EF Core та SQL Server, не пробиваючись через надто загальні інтерфейси.
- Транзакційні сценарії залишаються явними: контролер відкриває транзакцію, але доменна модель виконує змістовну роботу.
- Код ближчий до реального бізнес-процесу: документ проводиться, рядок списує товар, складський запис змінює залишок, журнал фіксує рух.
Звичайно, такий підхід має свою ціну. Доменна модель стає залежною від EF Core, а отже менш придатною для повної заміни persistence-механізму. Але для конкретного класу систем це може бути прийнятним і навіть бажаним компромісом. Якщо головна цінність — надійна робота облікової системи під реальним навантаженням, то архітектура має служити цій меті, а не абстрактній можливості переписати persistence-рівень у майбутньому.
Тут важливо не протиставляти цей підхід Clean Architecture як «правильний» і «неправильний». Правильніше говорити про різні компроміси. Clean Architecture прагне захистити домен від зовнішніх деталей. Практика автора в цьому прикладі прагне захистити життєздатність самої бізнес-сутності, навіть якщо для цього сутність має тісніше взаємодіяти з EF Core і базою даних.
Важливі зауваження
- Користувачу на підприємстві абсолютно байдуже, яка архітектура продукту, з яким він працює щодня. Для нього важливі три речі — потрібний функціонал, зручний інтерфейс і швидкість відгуку.
- Для власника бізнесу важлива ціна використання та володіння продуктом. Навіть співвідношення ціна/ефективність його цікавить менше, бо це складно порахувати.
- Для менеджменту компанії-споживача важлива швидкість модифікацій і доробок під нові потреби бізнесу.
Де в цих вимогах до продукту є наявність «чистої архітектури», де потреба в абстрактних unit-тестах та відірваності від фреймворку?
Найбільш цікавим є третій пункт. Щоб забезпечити швидкість модифікацій і легкість розвитку системи, потрібна перш за все правильна організація абстракцій в класах бізнес-логіки.
Наприклад, «Документ -> Товарний документ -> Зовнішній товарний документ -> Видатковий зовнішній товарний документ -> Видаткова накладна». (Під «зовнішній документ» має взаємодіяти з зовнішнім контрагентом (постачальником або покупцем), на відміну від «внутрішнього документа», де така взаємодія відсутня, наприклад, «акт переоцінки товару»). Коли вас попросять реалізувати документ «Повернення постачальнику», достатньо створити ще один клас, і віднаслідуватись від «Видаткового зовнішнього товарного документа». Основна логіка в якому вже реалізована. І додати в новий клас лише той код, який реалізує особливості функціоналу лише цього документа.
Про архітектуру пишуть всі. А про правильну організацію ієрархії сутностей, на яких принципах це будувати, що потрібно закладати в базові класи — пишуть дуже мало. Бо, по перше — це складно. Треба дуже добре знати предметну область, а по друге — треба добре вміти працювати з абстракціями, знати де, і на якому рівні абстракції, повинні знаходитись певні властивості і певний функціонал. А це набагато важливіше. І саме правильна ієрархія бізнес-сутностей, правильна бізнес-архітектура забезпечить вам гнучкість продукту і швидкість його розвитку.. Тому, справді хороший складний продукт (зауважте, не проект, а продукт) створюється лише навіть не на другий раз, а десь на третій. Наприклад, Trade Control Utility — це вже втретє повністю переписаний проект.
Відірваність від фреймворку стала не стільки інженерним принципом, скільки архітектурним символом. Люди часто повторюють її як догму, не ставлячи головне питання: яка саме залежність є реально небезпечною в цьому конкретному проєкті?
Бо залежність залежності різниця.
Залежність бізнес-логіки від випадкового UI-фреймворку, тимчасової бібліотеки авторизації або стороннього SDK — це справді ризик. Сьогодні одна бібліотека, завтра інша, післязавтра сервіс закрився або змінив API.
Але EF Core — це інша категорія. Це не дрібна допоміжна бібліотека. Для бізнес-системи, яка живе навколо даних, документів, проводок, залишків, звітів і транзакцій, EF Core стає частиною робочого середовища системи. Так само як SQL Server, індекси, транзакції, навігаційні властивості, change tracking, LINQ-запити,Include,AsNoTracking,ExecuteUpdate,ToQueryString() і так далі.
Тому, коли кажуть:
домен не повинен залежати від EF Core,
це звучить красиво. Але в реальній обліковій системі виникає питання:
а що саме ми виграємо, якщо сховаємо EF Core за власною абстракцією?
Часто відповідь неприємна: майже нічого, зате втрачаємо дуже багато.
Особливо у звітності. У великих бізнес-системах значна частина коду — це не «чиста бізнес-логіка» в стиліorder.Confirm(), а побудова складних вибірок, групувань, фільтрів, агрегатів, періодів, залишків, оборотів, порівнянь, деталізацій. І там головне питання не в тому, чи можна замінити EF Core на щось інше, а в тому:
чи піде запит у SQL оптимально?чи використає індекс?чи не буде N+1?чи не буде зайвого materialize?чи не потягне зайві таблиці?чи не виконається частина логіки на клієнті?чи не треба розбити запит?чи не треба зробити raw SQL?чи правильно працює транзакція?У такому коді «незалежність від фреймворку» часто перетворюється на самообман. Можна написати інтерфейс:
IReportRepositoryале всередині все одно буде EF Core, SQL, індекси, специфіка провайдера, плани виконання,GROUP BY,JOIN,SUM,COUNT,HAVING,ROW_NUMBER() і так далі.
Тобто зовні воно ніби «чисте», а всередині все одно повністю залежить від бази й ORM. Просто ця залежність стає прихованою.
І це навіть гірше. Бо коли залежність явна, її можна контролювати. А коли вона замаскована абстракцією, розробник може думати, що має універсальний код, хоча насправді має SQL-залежний код, тільки загорнутий у красивий інтерфейс.
Щодо переходу на інший фреймворк. У більшості реальних систем імовірність заміни EF Core на інший ORM дуже низька. Особливо якщо проєкт уже багато років працює, команда знає EF, кодова база велика, звітність зав’язана на LINQ/SQL, а бізнес не готовий платити за «архітектурну чистоту», яка не дає прямого результату.
А от імовірність того, що система за
Якщо ймовірність переходу з EF Core майже нульова, то створення великого шару абстракцій «про всяк випадок» — це не архітектурна мудрість, а передчасне ускладнення.
Чому тоді всі так зосереджені на Clean Architecture? Причин кілька.
Перша — її легко пояснювати на простих прикладах.Order,Customer,Product,Repository,Handler,Controller — виглядає красиво, зрозуміло, структуровано. На рівні навчальної статті це створює відчуття архітектурної завершеності.
Друга — вона добре продається як «універсальна відповідь». Людям подобаються схеми, де все розкладено по шарах і є відчуття контролю. Це психологічно привабливо.
Третя — багато хто плутає архітектурну дисципліну з механічним дотриманням шаблону. Тобто замість того, щоб думати про ризики, зв’язність, цілісність даних, продуктивність і життєздатність сутностей, люди просто малюють шари й кажуть: «ось тепер у нас Clean Architecture».
Четверта — так, частково це справді ефект студентських або напівнавчальних статей. Людина дізналася проDomain,Application,Infrastructure,Dependency Inversion,Repository Pattern — і здається, що знайдено універсальний рецепт. Але досвід реальних систем швидко показує, що срібної кулі немає.
Проблема починається тоді, коли цей підхід застосовують однаково до всього: до доменної логіки, до звітності, до складних SQL-запитів, до високонавантажених документів, до масових обробок. Там, де дані є не «зовнішньою деталлю», а серцевиною системи, надмірна ізоляція від EF Core може не допомагати, а шкодити.
Висновки
Clean Architecture не є поганою архітектурою. Навпаки, вона формулює важливі ідеї: контроль залежностей, розділення відповідальності, незалежність ядра від зовнішніх деталей, тестованість бізнес-правил.
Але жодна архітектура не повинна застосовуватися механічно. У високонавантажених бізнес-системах важливі не тільки шари й абстракції, а й ефективність роботи з даними, цілісність транзакцій, зрозумілість коду, контроль конкурентного доступу та реальна поведінка системи у виробничих умовах.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
10 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівDDD -> God objects. При розмовах про ЧА чомусь упускають розподіл на модулі (аплікейшени), вертикальні слайси (адже у нас на будь-якому проекті є crud сутності- для них теж городити репозиторії і абстракції?).
Плюс зазвичай основна проблема- саме в бізнес-сценаріях — про них ЧА також нічого не каже. Ну тобто ми розділили по шарах — і виявляється, що 95% коду — це саме бізнес-логіка. Ну і? Чим допомогло нам розділення на шари?
Основна проблема ЧА — вона приблрала фокус з дійсно важливих речей на безкінечні розмови про ЧА та досягнення цієї чистоти.
Дякую за зауваження.
Тут треба зазначити, що чиста архітектура це не стільки розділення на шари, скільки відокремлення чистої бізнес-логіки від фізичної реалізації. Щоб бізнес сутність нічого не знала про базу даних, про те, як дані зберігаються. Тоді ви можете використовувати будь яке сховище, будь яку бібліотеку доступу до даних, будь яку ORM.
Але я підняв просте питання — а чи дійсно воно нам треба? Наскільки є критичним і життєво необхідним нам таке «відривання хати» (Нечуй-Левицький, «Кайдашева сім’я»).
А от розділення на шари дуже важливе.
Наприклад, веб-метод контролера повинен викликати метод ядра для проведення документа. Веб-метод повинен бути тонким і вміщувати лише той код, що стосується веб-метода. Наприклад, створення контекста бази даних, отримання з бази певного об’єкта, наприклад, документа. А потім нам потрібно цей документ провести по системі.
Таким чином вміст веб-метода буде мати щось типу
[HttpGet("[action]")] public async Task<IActionResult> Approve(long id, CancellationToken cancellationToken) { try { using UserTcuContext userTcuContext = await CreateTcuContext(cancellationToken); using IDbContextTransaction transaction = await userTcuContext.Database.BeginTransactionAsync(cancellationToken); Document document = await Document.GetItem(userTcuContext, id, cancellationToken); await document.Approve(cancellationToken); await transaction.CommitAsync(cancellationToken); return GetObjectResult(userTcuContext, 0); } catch (Exception ex) { return GetExceptionResult(ex); } }Мінімум операційОтримали контекст бази даних, відкрили транзакцію, знайшли документ за його Id і провели цей документ.
Якщо на якомусь етапі виникла виключна ситуація, перехопили її в catch, і далі обробили (записали помилку в лог і відправили користувачу об’єкт з помилкою).
Тобто всередині веб-метода ніяких змін товарних залишків не відбувається (коли, наприклад, відвантажуємо товар), це робиться на рівні самого документа в методі Approve
Контролер займається справами свого рівня, документ — справами свого рівня.
Для чого це потрібно?
Справа в тому, що документ може проводитись не лише через команду користувача через веб-метод.
Проведення може відбуватись через призначене завдання у фоновому режимі, коли настане його час. Або коли ми підписані на певну подію в черзі повідомлень (в шині подій типу RabbitMQ). А ще ми можемо мати декілька серверних додатків, що використовують те саме ядро бізнес-логіки.
Цим ми намагаємося досягнути головного — упаковувати складність (не вдаватись в деталі реалізації методів, повторно використовувати ті самі блоки коду і т.д.). Саме для цього і відбувається розділення на шари, використання ООП і іншого.
Насправді, вона не формулює, а плагіатить Onion Architecture, котра використала принципи Domain-Driven Design для розділення ядра Ports and Adapters на шари herbertograca.com/...-the-shoulders-of-giants
Дякую, Денисе, за відгук.
Я б не вважав це плагіатом, ми всі вивчаємо чужий досвід і на його підставі в нас народжуються нові ідеї. Я десь читав порівняння «чистої» та onion-архітектури. Так, вони дійсно схожі, ідеї майже ті самі — відокремити бізнес-логіку від «фізики». Іде постійний теоретичний пошук хороших рішень. Щось приживається, щось ні.
Щось я так не зрозумів, а де тут догми в статті. Поставили запитанпя про архітектуру системи — найвищий рівень проектування якій визначає склад программно-аппаратного комплексу, структуру компонентів і методи взаємодії між ними і усе.
А далі йде великий перелік технік оптимізацій під загально відому трерівневу архітектуру Web застосунку : Data persistence layer -> Buiseness Logic Layer -> Presentation Layer.
Я вам навіть більше скажу, коли ви таке розробляєте — ви компонуєте по шаблонах те що розробили і транслювали вам системою освіти, хтось типу Крісса Річардсона з Amazon, Будді Курнівана з BeaSystems (поглинена Oracle) і т.д.
Тобто задались стратігічними питаннями техліда, рально спустились на рівень де — мілди з джуніорами.
Дякую за відгук. Сподіваюсь, що в статті догм немає. Ідея статті в тому, що не кожен проект повинен слідувати шаблонам і практикам «чистої архітектури». Що раціональна доцільність важливіша за той чи інший шаблон.А також те, що можна побудувати ефективний продукт, максимально скоротивши кількість шарів абстракцій лише до необхідного рівня.
«Clean Code» та «Clean Archirecture» — це в сутності маркетинговий бренд двух відомих книг бестселерів, авторства консультанта і методиста Роберта Мартіна, відомого як Дядько Боб.
res.cloudinary.com/.../y8xpcouk1zaveptq6fyd.png
Точно, куди ж нам без них.
Але скажу вам, наскільки легко читається «Чистий код», настільки важко читається «Чиста архітектура». Відчуття таке, що Мартін тягнув туди все, що треба і не треба, все, що стосується архітектури і не стосується.
Більшість з того, що написано в його «архітектурі» мені було давно відомо, і не було зрозуміло, для чого він це в архітектуру потягнув. Наприклад, там багато розписано про ООП, хоча це окрема тема і до архітектури прямого відношення не має. Але я чесно її пройшов, хоча іноді доводилось себе примушувати.
Дуже цікава публікація. На доу вкрай мало пишуть про архітектуру. Є таке доповнення. Автор пише про багатошаровість тільки текстом, але більш наочно це зробити у вигляді малюнка з відображенням шарів та моделей даних та схеми взаємодії між ними. Наприклад послідовність «Документ -> Товарний документ -> Зовнішній товарний документ -> Видатковий зовнішній товарний документ -> Видаткова накладна» показати на малюнку у вигляді взаємодії між шарами та моделями даних. Як приклад малюнки у публікації habr.com/ru/articles/1005628
Дякую за відгук, пане Олексію.
Я зроблю невеличкий проект у якості прикладу з повним циклом. Спробую знайти час впродовж тижня і викладу сюди посилання.
Що стосується «чистої архітектури», часто це зводиться до абсурду.
Нам зараз на сайт потрібен AI Asistant, який би по матеріалах сайту відповідав би на запитання відвідувачів. Проект лінійний та тривіальний, писати вручну не хотілось, дав задачу в Chat GPT. Він мені нагородив цілого монстра з чистою архітектурою. Все доведено до абсурду, на будь який клас створені інтерфейси. Невеличкий проект розбух до величезного, при тому, що корисного коду там, який щось дійсно робить, відсотків 10.
Після цього дав до Chat GPT такий промпт, щоб він не мудрував, і зробив те що працює і працює зрозуміло.
Промпт такий
Давай зробимо так
1. Ти забуваєш про цю чисту архітектуру, де все зроблено через інтерфейси. Я не планую змінювати ef core на щось інше. Я не планую змінювати Open AI на щось інше. Зроби всю взаємодію через звичайні методи звичайних класів.
2. Не створюй зайвих сутностей. Я так розумію, що нам після кожного запиту клієнта треба зробити наступні дії
2.1. По запиту клієнта формуємо summary з попередніх запитів і відповідей через Open AI(якщо вони є, якщо нема, крок пропускаємо)
2.2. Створюємо Embedding для запиту клієнта.
2.3. Знаходимо векторну близкість до фрагментів тексту наших матеріалів.
2.4. Створюємо промпт до Open AI зі запитом покупця і фрагментами відповідей
2.5. Відправляємо це до OpenAI і отримуємо відповідь.
2.6. Зберігаємо в базі даних запитання, відповідь і супутні дані (summary, токени та інше)
2.7. Повертаємо клієнту відповідь у веб-методі контролера. Подивись, як організована архітектура у проекті BillingServer2018.csproj в папці BillingServer2 За тими принципами перепиши наш проект. Не забудь додавати коментарі, кодування тексту зроби UTF
І все. Ніякої чистої архітектури. Я не кажу, що вона погана. Але мені вона для цього проекту не потрібна. В цьому ідея статті — робити простіше, не ускладнювати. Принцип Оккама — не породжувати нову сутність без крайньої необхідності.