Глава 8. Архітектурні Патерни
Глава 8. Архітектурні Патерни
"Архітектура — це рішення, які важко змінити пізніше. Тому варто приділити їм увагу зараз." — Мартін Фаулер
У попередніх главах ми фокусувалися на тактичних патернах (Tactical Patterns) — будівельних блоках, які допомагають нам моделювати бізнес-логіку всередині одного Обмеженого Контексту (Bounded Context). Ми говорили про Агрегати, Сутності, Об'єкти-Значення та Доменні Сервіси. Це наші "цеглинки" та "розчин".
Але як нам скласти ці цеглинки в стійку будівлю? Як організувати взаємодію між бізнес-логікою, базою даних, користувацьким інтерфейсом та зовнішніми API так, щоб система не перетворилася на "Велику Грудку Бруду" (Big Ball of Mud)?
У цій главі ми піднімемося на рівень вище і розглянемо архітектурні патерни. Це стратегії організації кодової бази, які визначають, як різні частини системи спілкуються одна з одною.
1. Вступ та Контекст
Проблема: Хаос залежностей
Уявіть, що ви пишете простий скрипт для автоматизації. Ви змішуєте логіку (як обробити дані), введення (звідки взяти дані) і виведення (куди покласти результат) в одному файлі. Це працює для 100 рядків коду.
Тепер уявіть Enterprise-систему на 500,000 рядків коду. Якщо бізнес-логіка напряму викликає базу даних, а UI напряму звертається до бізнес-правил, які, в свою чергу, малюють HTML — ви отримуєте систему, яку неможливо протестувати, неможливо змінити і страшно розгортати.
Жорстка Зв'язність (High Coupling)
Низька Зв'язність (Low Cohesion)
Мета: Організований Хаос
Архітектурні патерни дають нам:
- Структуру: Чіткі місця для кожного типу коду.
- Розділення Відповідальності (SoC): Кожен модуль робить одну річ і робить її добре.
- Тестованість: Можливість перевірити бізнес-логіку без підняття бази даних чи веб-сервера.
Ми розглянемо три фундаментальні підходи, кожен з яких еволюціонував з попереднього:
Шарувата Архітектура (Layered Architecture)
Класичний підхід "торта". Простий для розуміння, ідеальний для простих CRUD-додатків, але має свої обмеження.
Порти та Адаптери (Ports and Adapters)
Також відома як Гексагональна або Чиста Архітектура. Інвертує залежності, ставлячи домен у центр всесвіту.
CQRS (Command-Query Responsibility Segregation)
Розділення системи на дві частини: одну для зміни стану (Write), іншу для читання (Read). Вища ліга для складних доменів.
2. Співставлення Бізнес-Логіки та Архітектури
Бізнес-логіка — це серце вашого програмного забезпечення, але це не єдиний орган. Системі потрібні "руки" (UI), "пам'ять" (DB) та "вуха" (API).
Різноманітність завдань створює спокусу розподілити бізнес-логіку де попало:
- Трохи валідації в JavaScript на фронтенді.
- Трохи розрахунків у контролері.
- Трохи логіки в збережених процедурах SQL.
Це шлях до пекла підтримки (Maintenance Hell).
Архітектурні патерни вводять правила гри. Вони кажуть: "Ти, UI, можеш говорити тільки з Сервісом. А ти, Сервіс, нічого не знаєш про Базу Даних напряму".
3. Шарувата Архітектура (Layered Architecture)
Це найстаріший, найвідоміший і досі найпопулярніший патерн. Якщо ви коли-небудь створювали папку Models, Views та Controllers (MVC) — ви вже частково знайомі з ідеєю шарів, хоча N-Tier архітектура йде трохи далі.
Концепція
Уявіть геологічні шари землі або, що смачніше, торт "Наполеон". Кожен шар лежить на іншому. Верхній шар спирається на нижній, але нижній шар нічого не знає про верхній.
У класичній формі ми маємо три основні шари (Layers):
- Шар Представлення (Presentation Layer - PL)
- Шар Бізнес-Логіки (Business Logic Layer - BLL)
- Шар Доступу до Даних (Data Access Layer - DAL)
Анатомія Шарів
1. Шар Представлення (Presentation Layer)
Це "обличчя" вашої системи. Тут відбувається взаємодія з зовнішнім світом.
Що тут живе:
- Веб-контролери (MVC Controllers).
- REST API ендпоінти.
- gRPC сервіси.
- CLI команди.
- Consumer-и черг повідомлень (Kafka, RabbitMQ).
Відповідальність:
- Прийняти запит (HTTP, Command, Message).
- Провалідувати формат вхідних даних (але не бізнес-правила!).
- Перетворити дані у формат, зрозумілий бізнес-логіці (DTO -> Domain Model).
- Викликати бізнес-логіку.
- Перетворити результат назад у формат відповіді (Domain Model -> View Model / JSON).
- Обробити помилки та повернути відповідний статус (404, 500).
if (user.balance < price), ніяких розрахунків знижок. Його робота — бути перекладачем і диспетчером.2. Шар Бізнес-Логіки (Business Logic Layer)
Це "мозок" системи. Тут приймаються рішення.
Що тут живе:
- Сутності (Entities).
- Об'єкти-Значення (Value Objects).
- Доменні Сервіси (Domain Services).
- Сценарії Транзакцій (Transaction Scripts).
Відповідальність:
- Реалізація бізнес-правил та інваріантів.
- Виконання обчислень.
- Координація змін стану.
Як зазначає Ерік Еванс, цей шар є серцем програмного забезпечення. Саме тут реалізуються патерни, які ми розглядали в главах 5-7 (Active Record або Domain Model).
3. Шар Доступу до Даних (Data Access Layer)
Це "сховище" системи.
Що тут живе:
- Репозиторії (Repositories).
- Data Access Objects (DAO).
- ORM Mapping конфігурації (EntityFramework Core, Hibernate).
- Клієнти до зовнішніх API (якщо вони розглядаються як джерело даних).
Відповідальність:
- CRUD операції (Create, Read, Update, Delete).
- Побудова SQL запитів.
- Мапінг результатів з бази даних у об'єкти в пам'яті.
!NOTE У сучасних реаліях DAL — це не тільки SQL база даних. Це може бути NoSQL (MongoDB), пошукового індексу (Elasticsearch), кешу (Redis) або навіть хмарного сховища файлів (S3). Для шару бізнес-логіки деталі реалізації DAL мають бути сховані за абстракцією (хоча в класичній Layed Architecture це часто порушується).
Правила Комунікації
Головне правило Шаруватої Архітектури: Залежності спрямовані зверху вниз.
- Шар Представлення знає про Шар Бізнес-Логіки.
- Шар Бізнес-Логіки знає про Шар Доступу до Даних.
- Шар Доступу до Даних не знає нікого вище себе.
Це забезпечує певну ізоляцію. Якщо ви зміните верстку HTML у PL, це не вплине на BLL. Якщо ви оптимізуєте SQL запит у DAL, BLL і PL про це навіть не дізнаються (теоретично).
Сувора vs. Послаблена Шаруватість (Strict vs. Relaxed Layers)
- Сувора (Strict): Шар може звертатися тільки до шару, що безпосередньо під ним. PL -> BLL -> DAL. PL не може викликати DAL напряму.
- Послаблена (Relaxed): Шар може звертатися до будь-якого шару, що знаходиться нижче. PL може викликати DAL для простих запитів на читання (наприклад, для заповнення dropdown-списку).
Варіація: Сервісний Шар (Service Layer)
Часто трьох шарів недостатньо. Коли бізнес-логіка складна, контролери стають занадто товстими, або бізнес-сутності починають займатися оркестрацією. Тут на сцену виходить Сервісний Шар.
Це додатковий прошарок між PL та BLL.
"Сервісний шар визначає межу додатка і набір доступних операцій з точки зору клієнтів інтерфейсу." — Мартін Фаулер
Роль Сервісного Шару
Він виступає як Оркестратор. Він не містить "чистої" бізнес-логіки (правил розрахунку цін, валідації стану сутності), він містить логіку сценаріїв використання (Use Case Logic).
Типовий код методу в Service Layer:
- Відкрити транзакцію.
- Завантажити Агрегат з Репозиторію (через DAL).
- Викликати метод бізнес-логіки в Агрегаті (зробити дію).
- Зберегти зміни через Репозиторій.
- Закомітити транзакцію.
- (Опціонально) Відправити email або подію.
Приклад Рефакторингу: Від "Товстого Контролера" до Сервісу
Розглянемо приклад створення користувача.
Варіант 1: Логіка в Контролері (Погано)
// MvcApplication.Controllers.UserController
public class UserController : Controller
{
private readonly DbContext _db;
[HttpPost]
public ActionResult Create(ContactDetails contactDetails)
{
// 1. Початок інфраструктурної роботи
using var transaction = _db.Database.BeginTransaction();
try
{
// 2. Створення сутності (Бізнес-логіка)
var user = new User();
user.SetContactDetails(contactDetails);
// 3. Збереження (DAL)
_db.Users.Add(user);
_db.SaveChanges();
transaction.Commit();
return View("Success");
}
catch (Exception ex)
{
transaction.Rollback();
return View("Error", ex);
}
}
}
Проблеми:
- Контролер знає про транзакції БД.
- Контролер прив'язаний до Entity Framework (
DbContext). - Цю логіку неможливо перевикористати (наприклад, з CLI утиліти або фонового воркера).
Варіант 2: Виділення Сервісного Шару (Краще)
Ми створюємо UserService, який інкапсулює "Сценарій Створення Користувача".
// ServiceLayer.UserService
public class UserService
{
private readonly IUserRepository _repository;
private readonly ITransactionManager _transactionManager;
public OperationResult Create(ContactDetails contactDetails)
{
try
{
_transactionManager.Begin();
var user = new User();
user.SetContactDetails(contactDetails);
_repository.Add(user);
_transactionManager.Commit();
return OperationResult.Success();
}
catch (Exception ex)
{
_transactionManager.Rollback();
return OperationResult.Failure(ex);
}
}
}
// MvcApplication.Controllers.UserController
public class UserController : Controller
{
public ActionResult Create(ContactDetails contactDetails)
{
// Контролер просто делегує виконання
// Вся "брудна" робота схована в сервісі
var result = _userService.Create(contactDetails);
return result.IsSuccess ? View("Success") : View("Error", result.Error);
}
}
Переваги:
- Reusability:
UserServiceможна викликати з MVC, Web API, gRPC, Console App. - Separation: Контролер чистий, він займається лише HTTP/View питаннями.
- Testing:
UserServiceлегко протестувати юніт-тестами, замокавши репозиторій.
Коли використовувати Шарувату Архітектуру?
Це чудовий старт.
✅ За:
- Простота розуміння командою.
- Стандарт де-факто для багатьох фреймворків (Rails, Django, Spring MVC, ASP.NET MVC).
- Добре підходить для патернів Transaction Script та Active Record.
❌ Проти:
- Database Driven Development: Оскільки BLL залежить від DAL, ми часто починаємо проектування з таблиць у базі даних, а не з поведінки об'єктів.
- Труднощі з Domain Model: Якщо ви хочете справжню чисту Доменну Модель, яка не залежить від інфраструктури, шарувата архітектура ставить палки в колеса, бо змушує бізнес-шар залежати від шару даних.
- Транзитивні залежності: Зміни в DAL можуть каскадом ламати BLL і PL.
Термінологічна плутанина
У різних джерелах шари називаються по-різному. Ось "словник перекладу":
| Ваше джерело | Ця книга (DDD) | Альтернативи |
|---|---|---|
| Presentation | Presentation Layer | UI Layer, Web Layer, Interface Layer |
| Logic | Business Logic Layer | Domain Layer, Model Layer, Core |
| Application | Service Layer | Application Layer, Use Case Layer |
| Data | Data Access Layer | Infrastructure Layer, Persistence Layer |
- Layers (Шари): Логічний поділ коду. Вони можуть жити в одній DLL або одному моноліті.
- Tiers (Рівні): Фізичний поділ. Наприклад, Web Server (Tier 1) -> App Server (Tier 2) -> Database Server (Tier 3). Це різні машини/процеси.
4. Порти та Адаптери (Ports and Adapters)
Класична шарувата архітектура має фатальний недолік для складних доменів: залежність бізнесу від даних. Уявіть, що ваша бізнес-логіка (найцінніше, що у вас є) залежить від бібліотеки доступу до бази даних (найбільш мінливої і технічної частини). Якщо ви хочете замінити SQL на NoSQL, вам доведеться переписувати бізнес-логіку або принаймні перекомпілювати її. Це неправильно.
Принцип Інверсії Залежностей (Dependency Inversion Principle - DIP) каже:
"Високорівневі модулі не повинні залежати від низькорівневих. Обидва повинні залежати від абстракцій."
Архітектура Порти та Адаптери (також відома як Гексагональна Архітектура, Цибулева Архітектура, Чиста Архітектура) вирішує цю проблему радикально.
Зміна Парадигми
Ми перестаємо думати про систему як про "пиріг" шарів (зверху вниз). Ми думаємо про неї як про Ядро (Core), яке знаходиться в центрі, та Зовнішній Світ (Infrastructure), який його оточує.
Концепція
Уявіть ігрову консоль.
- Ядро (Консоль): Це бізнес-логіка ігор. Вона не знає, який телевізор ви підключите (Sony, Samsung) і який джойстик використаєте (оригінальний, китайський).
- Порти: Гнізда на консолі (HDMI, USB). Це контракти (інтерфейси), які визначає консоль. "Якщо хочеш отримати картинку, встав кабель, який підходить у це гніздо".
- Адаптери: Самі кабелі та драйвери. Вони адаптують сигнали зовнішніх пристроїв до портів консолі.
У цій архітектурі Бізнес-Логіка стає центром всесвіту. Вона не залежить ні від чого. Вона визначає правила гри.
Зверніть увагу на стрілки на діаграмі. Всі залежності спрямовані всередину, до Ядра. Інфраструктура залежить від Домену. Домен не залежить від Інфраструктури.
Реалізація DIP (Dependency Inversion)
Як це працює на рівні коду? Давайте порівняємо.
У Шаруватій архітектурі (неправильно для DDD)
namespace App.BusinessLogic
{
// Бізнес-логіка залежить від БД
using App.DataAccess;
public class OrderService
{
// ПРЯМА залежність від класу з DAL!
private SqlRepository _repo = new SqlRepository();
public void PlaceOrder(Order order)
{
if (!order.IsValid()) throw new Exception();
_repo.Save(order);
}
}
}
У Портах та Адаптерах (правильно для DDD)
1. Порт (Інтерфейс) в Ядрі: Спочатку бізнес-логіка каже: "Мені потрібно вміти зберігати замовлення. Я не знаю як, і мені байдуже куди (файл, БД, хмара). Ось контракт:"
namespace App.Core.Ports
{
public interface IOrderRepository
{
void Save(Order order);
}
}
2. Бізнес-логіка використовує Порт:
namespace App.Core.Services
{
using App.Core.Ports;
public class OrderService
{
private readonly IOrderRepository _repo; // Залежність від ІНТЕРФЕЙСУ
// Ми отримуємо реалізацію через конструктор (Dependency Injection)
public OrderService(IOrderRepository repo)
{
_repo = repo;
}
public void PlaceOrder(Order order)
{
if (!order.IsValid()) throw new Exception();
_repo.Save(order);
}
}
}
3. Адаптер (Реалізація) в Інфраструктурі: Тільки тепер, в окремому проекті або папці, ми створюємо клас, який знає про базу даних.
namespace App.Infrastructure.Adapters
{
using App.Core.Ports;
using Microsoft.EntityFrameworkCore;
// Адаптер реалізує Порт
public class SqlOrderRepository : IOrderRepository
{
private readonly DbContext _db;
public SqlOrderRepository(DbContext db) => _db = db;
public void Save(Order order)
{
// Перетворюємо Доменну модель в SQL запит
var record = MapToSqlRecord(order);
_db.Orders.Add(record);
_db.SaveChanges();
}
}
}
Інтеграція Інфраструктурних Компонентів
Поняття "Порти та Адаптери" покриває два напрямки комунікації:
1. Первинні (Driving) Адаптери
Це те, що викликає вашу програму. Вони "керують" вашим додатком.
- Хто: Користувач через UI, тестовий фреймворк, зовнішній Webhook, CRON-job.
- Як: Вони викликають методи Прикладного Шару (Application Service) через вхідні Порти.
- Приклад: MVC Controller адаптує HTTP запит у виклик методу
OrderService.PlaceOrder().
2. Вторинні (Driven) Адаптери
Це те, що викликається вашою програмою. Вони "керовані" вашим додатком.
- Хто: База даних, SMTP сервер, Шина повідомлень, файлова система.
- Як: Ваше Ядро викликає інтерфейс (Вихідний Порт), а Адаптер реалізує цей інтерфейс.
- Приклад:
SqlOrderRepositoryреалізуєIOrderRepository.SmtpEmailSenderреалізуєIEmailSender.
Варіації: Гексагональна, Цибулева, Чиста
Давайте розберемося в цьому зоопарку назв. Вони описують одну й ту саму суть, але з різними метафорами.
Гексагональна Архітектура (Alistair Cockburn)
Цибулева Архітектура (Jeffrey Palermo)
Розвиває ідею кілець.
- Центр: Domain Model (лише об'єкти і поведінка).
- Кільце 2: Domain Services (складна логіка).
- Кільце 3: Application Services (оркестрація).
- Зовнішні кільця: UI, Tests, Infrastructure. Головне правило: усі залежності спрямовані до центру.
Чиста Архітектура (Uncle Bob)
Найбільш відома версія. Об'єднує ідеї попередніх.
- Entities: Enterprise Business Rules.
- Use Cases: Application Business Rules.
- Interface Adapters: Controllers, Gateways, Presenters.
- Frameworks & Drivers: DB, Web, UI.
Суть у них одна й та сама: Бізнес-логіка ізольована від деталей реалізації.
Коли використовувати Порти та Адаптери?
Цей патерн ідеальний для систем, де ви використовуєте Domain Driven Design.
✅ Переваги:
- Тестованість: Ви можете протестувати все ядро, не маючи бази даних. Просто створіть
InMemoryOrderRepositoryабо Mock. Ваші тести будуть літати (мілісекунди замість секунд). - Гнучкість: Хочете замінити Oracle на PostgreSQL? Пишете новий адаптер. Хочете додати керування через Telegram-бота? Пишете новий вхідний адаптер. Ядро не змінюється.
- Відкладені рішення: Ви можете розробляти ядро системи, навіть не вирішивши, яку базу даних будете використовувати. Ви можете відкласти це рішення на місяці.
❌ Недоліки:
- Складність: Багато нових файлів (інтерфейси, DTO, мапери). Для простого "Hello World" або CRUD це оверхед (Over-engineering).
- Крива навчання: Розробникам потрібно звикнути до інверсії залежностей і розділення моделей.
- Debugging: Іноді важко прослідкувати потік виконання, бо ви кликаєте "Go To Implementation" і потрапляєте на інтерфейс, а не на код.
5. CQRS (Command-Query Responsibility Segregation)
CQRS — це один з найбільш неправильно зрозумілих патернів. Багато хто думає, що це про "дві бази даних" або "складну магію з чергами". Насправді, в основі лежить дуже проста ідея про розподіл обов'язків.
Фундаментальна Проблема
У звичайних системах ми використовуємо одну й ту саму модель (клас) і для читання, і для запису.
- Ми маємо клас
User. - Коли ми реєструємо користувача (Запис), нам потрібні поля
PasswordHash,Salt,RegistrationDate,IsActive. - Коли ми показуємо список користувачів в адмінці (Читання), нам потрібні
FullName,LastLogin, але точно не потрібенPasswordHash.
З часом наша модель User стає монстром. Вона обростає полями, потрібними тільки для UI, і методами, потрібними тільки для бізнес-логіки. ORM стягує зайві дані. Оптимізувати запис важко, бо це ламає читання, і навпаки.
Рішення: Розділяй і Володарюй
Бертран Мейєр (творця мови Eiffel) сформулював принцип CQS (Command-Query Separation) для методів класів:
"Функція повинна або змінювати стан об'єкта (Command), або повертати результат (Query), але не обидва одночасно."
CQRS переносить цей принцип на рівень Архітектури.
Ми створюємо дві окремі моделі:
- Command Model (Write Model): Оптимізована для зміни стану. Вона валідує правила, слідкує за консистенцією. Вона "сувора".
- Query Model (Read Model): Оптимізована для читання. Вона просто повертає дані, які хоче бачити UI. Вона може бути денормалізована. Вона "швидка".
Такий підхід дозволяє нам використовувати Polyglot Persistence (Багатомовне Збереження):
- Ми пишемо в надійну PostgreSQL (відповідає ACID вимогам для транзакцій).
- Ми читаємо з супер-швидкого Elasticsearch або Redis, куди дані потрапляють у готовому для відображення вигляді.
Анатомія CQRS
1. Модель Виконання Команд (Write Side)
Тут живе наш DDD. Агрегати, Сутності, Репозиторії. Це єдине джерело істини (Source of Truth).
- Мета: Гарантувати консистенцію даних та інваріантів.
- Поведінка:
void Register(User user). Якщо щось не так — кидаємо Exception. - Повернення значень: В ідеалі — нічого (void). На практиці — ID створеної сутності або статус операції (Result Pattern). Але ніколи не повертайте DTO для відображення на UI.
2. Моделі Читання (Read Side / Projections)
Тут живе те, що потрібно інтерфейсу.
- Мета: Максимально швидко віддати дані.
- Поведінка: Ніякої бізнес-логіки. Ніякої валідації. Просто
SELECT * FROM ... WHERE .... - Особливість: Моделі читання — це Проекції. Вони подібні до
Materialized Viewsв SQL. Вони є кешем. Вони "лише для читання".
Проектування Моделей Читання
Як дані потрапляють з Command Side в Read Side? Є два шляхи.
Синхронні Проекції
При оновленні даних ми одразу оновлюємо і таблицю для читання, і таблицю для запису в одній транзакції.
- Як: У тій же транзакції, де ми зберігаємо
User, ми оновлюємоUserView. - Плюс: Простота. Дані завжди актуальні (Strong Consistency).
- Мінус: Повільніший запис (бо треба писати в дві таблиці). Важко масштабувати.
Асинхронні Проекції (Event Base)
Коли Command Side змінює стан, вона публікує Подію (Domain Event: UserRegistered).
Спеціальний обробник (Projector) слухає цю подію і оновлює Модель Читання (наприклад, додає запис в Redis).
Евентуальна Узгодженість (Eventual Consistency): Це головна ціна асинхронності. Користувач може змінити ім'я, оновити сторінку і побачити старе ім'я, бо подія ще не долетіла до Read DB. Чи це проблема? Залежить від бізнесу. Для банківського рахунку — так. Для лайків у соцмережі — ні.
Коли використовувати CQRS?
CQRS — це потужний інструмент, але він додає багато складності.
✅ Застосовувати, коли:
- Високе навантаження на читання: Співвідношення Read/Write > 100:1. Ви можете масштабувати Read Side (10 серверів Elastic) окремо від Write Side (1 майстер PostgreSQL).
- Складний UI: Екран потребує даних з 10 різних агрегатів. Робити це через ORM і джойни в бізнес-моделі — біль. Краще мати готову плоску таблицю "під екран".
- Event Sourcing: Якщо ви використовуєте Event Sourcing, CQRS є обов'язковим, бо ви не можете робити запити по подіях ефективно.
❌ Уникати, коли:
- Простий CRUD додаток.
- Прості бізнес-правила.
- Команда не готова до асинхронності та Eventual Consistency.
6. Event Sourcing (Подієва Орієнтація)
Ми не можемо говорити про CQRS і не згадати Event Sourcing (ES). Хоча це окремий патерн, вони часто ходять парою, як Бонні і Клайд.
Що не так зі зберіганням стану?
У традиційних базах даних ми зберігаємо поточний стан.
- У таблиці
Usersє запис:{ Id: 1, Balance: 100 }. - Якщо ми змінимо баланс, ми затремо старе значення:
{ Id: 1, Balance: 150 }. - Ми втратили інформацію: Як ми дійшли до 150? Це було поповнення на 50? Чи переказ на 100 і списання 50?
Event Sourcing пропонує інший підхід: Зберігати не стан, а події, що призвели до цього стану.
Потік Подій (Event Stream)
Замість одного запису в таблиці, ми маємо серію подій:
AccountCreated { Id: 1, Owner: "Alice" }Deposited { Amount: 100 }Deposited { Amount: 50 }Withdrawn { Amount: 20 }
Щоб дізнатися поточний баланс, ми просто "програємо" (Replay) всі події з початку:
0 + 100 + 50 - 20 = 130.
Переваги ES
- Аудит з коробки: Ви ніколи не губите дані. Ви знаєте все, що відбувалося з системою. Історія змін вбудована в архітектуру.
- Часова Машина: Ви можете відновити стан системи на будь-який момент часу в минулому. "Яким був баланс Аліси минулого вівторка о 14:00?" — просто програйте події до цього моменту.
- Аналітика: Ви можете аналізувати поведінку користувачів постфактум. "Як часто користувачі додають товар у кошик, а потім видаляють його?" Зі звичайною БД ви цього не дізнаєтесь (бо видалення затирає дані), з ES — легко.
- Спрощення CQRS: Write Side просто пише події (дуже швидко). Read Side підписується на події і будує будь-які проекції.
Недоліки ES (Чому це не для всіх)
- Складність: Це повна зміна мислення. Ніяких
UPDATEчиDELETE. ТількиINSERT. - Версіонування подій: Що робити, якщо ви змінили структуру події
OrderPlaced? Вам потрібні стратегії міграції (Upcasting). - Snapshots (Знімки): Якщо у вас 1 мільйон подій, програвати їх щоразу довго. Треба робити "знімки" стану кожні 100 подій.
- Event Store: Вам потрібна спеціалізована база даних (EventStoreDB) або налаштування існуючої (PostgreSQL) для ефективної роботи з потоками.
- EDA: Сервіси спілкуються повідомленнями (RabbitMQ).
- Event Sourcing: Стан всередині сервісу зберігається як серія подій. Ви можете мати ES без мікросервісів, і мікросервіси (EDA) без ES.
7. Архітектурні Анти-патерни
Знати, як не треба робити, так само важливо, як знати правильний шлях. Ось найпопулярніші архітектурні "граблі".
1. Велика Грудка Бруду (Big Ball of Mud)
Це відсутність архітектури.
- Симптоми: Циклічні залежності, спагетті-код, ніхто не знає, де що лежить.
- Лікування: Тільки переписування по частинах (Strangler Fig Pattern).
2. Лазанья-Архітектура (Lasagna Architecture)
Це Шарувата архітектура, доведена до абсурду.
- Симптоми: Занадто багато шарів. Щоб передати рядок з UI в БД, треба пройти через Controller -> DTO -> Facade -> Service -> Domain -> Repository -> DAO -> ORM.
- Наслідок: Кожна зміна вимагає редагування 10 файлів.
- Лікування: Видаляйте непотрібні шари. Використовуйте Relaxed Layering.
3. Газопровід (Gas Pipeline)
Це коли у вас є мікросервіси, але вони настільки зв'язані, що один упав — все впало.
- Симптоми: Довгі ланцюжки синхронних HTTP викликів (
Service A -> Service B -> Service C). - Лікування: Асинхронність, черги повідомлень, кешування.
4. Анемічна Доменна Модель (Anemic Domain Model)
Ми говорили про це в попередніх главах, але це і архітектурна проблема.
- Симптоми: Вся бізнес-логіка в Сервісах, а Сутності — це пусті "мішки для геттерів і сеттерів".
- Наслідок: Порушення інкапсуляції, дублювання логіки.
- Лікування: Переносьте логіку в Сутності (Rich Domain Model).
8. Тестування в різних Архітектурах
Ваша стратегія тестування напряму залежить від обраної архітектури.
Піраміда Тестування
Майк Кон запропонував ідею піраміди:
- Unit Tests (Багато, швидкі)
- Integration Tests (Менше, повільніші)
- E2E Tests (Мало, дуже повільні)
Тестування в Шаруватій Архітектурі
Тут часто виникає проблема: Важко писати Unit-тести для BLL, бо він залежить від DAL.
- Вам доводиться мокати базу даних або репозиторії.
- Часто скочуються до Ice Cream Cone Anti-pattern (багато ручних/E2E тестів, мало юнітів), бо "все зав'язано на базу".
Тестування в Портах та Адаптерах
Це рай для тестувальника.
- Unit Tests: Ви тестите Domain Entities та Domain Services без жодних моків інфраструктури. Це чиста Java/C# логіка.
- Subcutaneous Tests (Підшкірні тести): Ви тестите Application Use Cases, підміняючи порти фейками (
FakeRepository). Ви перевіряєте бізнес-сценарії повністю, не піднімаючи БД. - Integration Tests: Перевіряють тільки Адаптери (чи правильно
SqlOrderRepositoryпише в PostgreSQL).
docker-compose up — ви на правильному шляху.9. Практикум: Рефакторинг Legacy в Чисту Архітектуру
Припустимо, у вас є типовий контролер на 1000 рядків, який робить все підряд. Як перейти до Портів та Адаптерів?
Крок 1: Виділення Сервісу (Extraction)
Винесіть всю логіку з контролера в LegacyUserService. Контролер має стати тонким (лише HTTP -> Service -> HTTP).
Це ще не DDD, це просто прибирання сміття.
Крок 2: Інверсія Залежностей (DIP)
Подивіться, від чого залежить LegacyUserService. Від DbContext?
- Створіть інтерфейс
IUserRepository(Порт) з методами, які ви використовуєте. - Створіть реалізацію
SqlUserRepository(Адаптер), перенісши туди EF код. - Впровадьте
IUserRepositoryв сервіс через конструктор.
Крок 3: Виділення Доменної Моделі
Зараз ваш сервіс, ймовірно, маніпулює даними напряму ("Transaction Script").
Знайдіть бізнес-правила (наприклад, "користувач не може мати від'ємний баланс"). Перенесіть цю логіку в сутність User.
Сервіс має лише завантажувати сутність, викликати метод сутності і зберігати її.
Крок 4: Розділення Command і Query (CQS)
Якщо у вас є методи, які лише читають дані для UI — винесіть їх в окремі UserQueryService або Query Handlers. Нехай вони повертають плоскі DTO.
Очистіть основний сервіс від методів читання.
10. Часті Питання (FAQ)
- Request/Response DTO (для API): в Presentation Layer.
- CQRS Read Models: в Query Layer (Application).
- Domain Objects: Тільки в Domain Layer. Ніколи не віддавайте їх назовні.
- Синтаксична валідація (чи є email валідним рядком): в Presentation Layer (FluentValidation).
- Семантична валідація (чи унікальний email): в Application Service (звернення до БД).
- Інваріанти (чи можна змінити статус на 'Active'): в Domain Entity.
11. Область Застосування та Гібридні Підходи
На завершення, важливо пам'ятати: Вам не потрібно обирати один патерн для всієї системи.
Сучасна архітектура — це комбінація.
- Core Domain (там де гроші): Використовуйте Порти та Адаптери + DDD + можливо, CQRS (якщо складно). Тут потрібна максимальна гнучкість і чистота.
- Generic Subdomain (адмінка, налаштування): Використовуйте просту Шарувату Архітектуру або просто CRUD. Тут не треба мудрувати.
- Supporting Subdomain (ETL процеси): Можливо, просто Transaction Scripts.
Vertical Slices (Вертикальні Зрізи)
Замість того, щоб ділити код горизонтально (всі контролери разом, всі сервіси разом), спробуйте ділити його вертикально — по фічах (Features).
У кожній фічі (Features/Orders/PlaceOrder, Features/Products/GetCatalog) ви можете обрати той архітектурний підхід, який підходить саме їй. Одна фіча може бути складною (DDD), інша — простою (SQL Script).
Архітектурна Еволюція
Не бійтеся починати з простого.
- Почніть з Шаруватої Архітектури.
- Коли логіка ускладниться, додайте Сервісний Шар.
- Коли залежність від БД стане проблемою, відрефакторіть ядро в Порти та Адаптери.
- Коли запити на читання стануть повільними, винесіть їх в окремі CQRS Проекції.
Найгірша архітектура — це та, яка вирішує проблеми, яких у вас ще немає.
Підсумок
- Шарувата Архітектура: Хороший старт, але обережно із залежностями від БД.
- Порти та Адаптери: Золотий стандарт для складного DDD. Інвертує залежності, оберігаючи бізнес-логіку.
- CQRS: Потужний інструмент для оптимізації читання і запису окремо. Використовуйте з розумом, пам'ятаючи про ціну складності.
Перевірка Знань
Моделювання фактора часу
Event Sourcing pattern для моделювання темпоральної dimensії в Domain-Driven Design
Глава 9. Патерни Взаємодії
У попередніх розділах (глави 5–8) ми фокусувалися на тактичних патернах — як будувати окремі компоненти системи, моделювати бізнес-логіку всередині одного Обмеженого Контексту (Bounded Context). Ми навчилися створювати Агрегати, Об'єкти-Значення та організовувати їх у Шарувату або Гексагональну архітектуру.