Глава 10. Проектні Евристики
Глава 10. Проектні Евристики
"It depends" (Це залежить) — найпопулярніша відповідь будь-якого консультанта чи архітектора. Але від чого саме це залежить?
Коли використовувати CQRS, а коли вистачить звичайного CRUD? Коли будувати складну Гексагональну Архітектуру, а коли просто писати код в контролерах? Коли мікросервіси — це благо, а коли — смерть проекту?
Ця глава — це набір Евристик (емпіричних правил), які допоможуть вам приймати зважені рішення. Ми перетворимо абстрактне "It depends" на конкретні алгоритми прийняття рішень.
Межі Контекстів
Бізнес-Логіка
Архітектура
1. Евристики Обмежених Контекстів
Одна з найчастіших помилок в DDD — нарізання системи на надто дрібні шматки (Entity Services) або створення одного гігантського моноліту.
Функція vs Дані
Не діліть контексти по сутностях (CustomerService, OrderService, ProductService).
Діліть по Бізнес-Можливостях (Business Capabilities) або Поведінці.
- ❌
ProductService: зберігаєid,name,price,stock. - ✅
CatalogContext: пошук, категорії, SEO-атрибути. - ✅
InventoryContext: залишки на складах, резервування, інвентаризація. - ✅
PricingContext: знижки, промо-акції, динамічне ціноутворення.
Розмір Команди (Число Данбара)
Контекст повинен бути таким, щоб його могла підтримувати одна команда (Two-Pizza Team, 5-9 людей). Якщо над контекстом працює 20 людей — це забагато когнітивного навантаження. Розбивайте його. Якщо над 5-ма контекстами працює одна людина — це оверхед на перемикання контексту та інфраструктуру. Об'єднуйте їх.
Життєвий Цикл (Rate of Change)
Якщо частина системи змінюється щодня (напр. фронтенд промо-акцій), а інша частина — раз на рік (напр. ядро бухгалтерського обліку), їх варто розділити. Різні темпи змін вимагають різних процесів релізу і тестування.
Когнітивне Навантаження (Team Topologies)
Згідно з книгою Team Topologies, межі сервісів повинні збігатися з межами когнітивного навантаження команди. Якщо команда "тоне" в підтримці, постійно перемикається між контекстами і боїться чіпати старий код — це сигнал, що межі контекстів прокладені неправильно.
Евристика Розміру
Контекст не є "завеликим", якщо:
- Команда розуміє його цілком.
- Новий розробник може розібратися в ньому за 1-2 тижні.
- Зміни в одній частині контексту не ламають іншу (High Cohesion).
2. Евристики Бізнес-Логіки
Не вся логіка однакова. Для "Core Domain" ми хочемо максимум гнучкості, для "Generic Subdomain" — купити готове або зробити швидко.
Матриця Cкладності
Вибирайте патерн реалізації в залежності від складності піддомену.
| Патерн | Складність Домену | Складність Структур Даних | Приклад |
|---|---|---|---|
| Transaction Script | Низька | Низька | Адмінка, прості звіти, ETL джоби |
| Active Record | Низька/Середня | Середня | Прості CRUD додатки, блоги |
| Domain Model | Висока | Висока | Core Domain, складна бізнес-логіка, інваріанти |
| Event Sourced | Дуже Висока | Часова (Temporal) | Бухгалтерія, Медичні картки, Git-подібні системи |
Transaction Script vs Domain Model
Давайте порівняємо код.
Transaction Script (Процедурний стиль)
Логіка розмазана по сервісу. Сутності тупі (DTO).
public class OrderService
{
public void SubmitOrder(int orderId)
{
var order = _db.Orders.Find(orderId);
var customer = _db.Customers.Find(order.CustomerId);
// Validation Logic
if (order.Total > customer.CreditLimit)
throw new Exception("Limit exceeded");
if (order.Items.Count == 0)
throw new Exception("Empty order");
// Logic
order.Status = "Submitted";
order.SubmittedDate = DateTime.Now;
_db.SaveChanges();
}
}
Проблема: Цей код неможливо перевикористати. Якщо інший сервіс захоче перевірити ліміт, він продублює if (order.Total > customer.CreditLimit).
Domain Model (ООП стиль)
Логіка інкапсульована в Агрегаті. Сервіс тільки координує.
public class Order : AggregateRoot
{
public void Submit(Customer customer)
{
// Invariant Guard
if (this.Items.IsEmpty)
throw new DomainException("Order cannot be empty");
// Rule checks utilizing other aggregates info
if (this.CalculateTotal() > customer.GetAvailableCredit())
throw new DomainException("Credit limit exceeded");
this.Status = OrderStatus.Submitted;
this.SubmittedDate = DateTime.UtcNow;
AddDomainEvent(new OrderSubmitted(this.Id));
}
}
// Application Service
public class SubmitOrderHandler
{
public void Handle(SubmitOrder cmd)
{
var order = _repo.Get(cmd.Id);
var customer = _customerRepo.Get(order.CustomerId);
order.Submit(customer); // All logic is here
_repo.Save(order);
}
}
Перевага: Логіка тестується юніт-тестами без бази даних. Правила живуть поруч з даними.
Анемічна Модель (Anemic Model) — Анти-патерн?
Мартін Фаулер називає Анемічну Модель (сутності тільки з геттерами/сеттерами) анти-патерном. Але в DDD це залежить від контексту.
- Для Generic Subdomain (адмінка) — це нормально.
- Для Core Domain — це зло, бо вся логіка перетікає в сервіси ("Transaction Scripts on steroids").
3. Евристики Архітектури
Архітектура — це про структурування коду.
Layered Architecture (Шарувата)
Підходить для більшості проектів на старті.
- Плюси: Зрозуміла всім, проста структура папок.
- Мінуси: Схильна до перетворення на "Database Driven Design" (все залежить від БД).
Hexagonal (Ports & Adapters)
Для систем, які живуть довго і мають складну бізнес-логіку.
- Плюси: Повна ізоляція домену. Легко тестувати. Легко замінити БД або API.
- Мінуси: Багато бойлерплейту (DTO, Mappers, Interfaces). Вищий поріг входу.
CQRS (Command Query Responsibility Segregation)
Використовувати тільки точково!
- Коли ТАК:
- Навантаження на читання в 100 разів вище за запис.
- Потрібні складні Materialized Views (звіти, екран "Dashboard").
- Команди і Запити мають кардинально різні вимоги до масштабування.
- Коли НІ:
- Простий CRUD.
- "Бо це модно".
Структура Проекту
Layered Architecture
src/
├── Controllers/ # Presentation
├── Services/ # Application Logic (Mix of Domain & App)
├── Entities/ # Data Models (Anemic)
├── Data/ # DbContext, Repositories
└── DTOs/ # Data Transfer Objects
Hexagonal Architecture (Approximation)
src/
├── Domain/ # Core Logic (No dependencies!)
│ ├── Model/ # Aggregates, Value Objects
│ └── Ports/ # Interfaces (IRepository, INotificationService)
├── Application/ # Use Cases (Orchestration)
│ ├── Commands/ # Handlers
│ └── Queries/
├── Infrastructure/ # Adapters
│ ├── Persistence/ # EF Core impl of Ports
│ └── Messaging/ # RabbitMQ adapters
└── WebApi/ # Entry Point
4. Стратегії Тестування
Як тестувати DDD-системи? Забудьте про "Test Pyramid" у її класичному розумінні (багато юнітів, мало інтеграційних), якщо ви робите мікросервіси.
The Testing Diamond (Ромб Тестування)
У мікросервісах одиницею ізоляції є Сервіс, а не клас. Тому Integration Tests стають найважливішими.
- Unit Tests: Тестуйте лише Domain Model (Агрегати, Value Objects) і складні алгоритми. Не тестуйте геттери/сеттери та прості мапери.
- Integration Tests (Component Tests): Підніміть контекст з реальною БД (TestContainers) і перевірте сценарії через публічний API (або Application Layer). Це дає найбільше впевненості.
- E2E Tests: Мінімум. Тільки критичні шляхи (Happy Path), які проходять через всю систему. Вони повільні і крихкі.
Component Tests (In-Process Integration)
Найкращий ROI (Return on Investment) дають тести, які піднімають весь Application Context в пам'яті, підміняють тільки зовнішні сервіси (Payment Gateway), але використовують реальну БД.
public class OrderComponentTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
// TestContainers for Postgres
// ... setup code ...
[Fact]
public async Task Should_create_order_when_valid_input()
{
// Arrange
var command = new CreateOrderCommand(...);
// Act
var response = await _client.PostAsJsonAsync("/orders", command);
// Assert
response.EnsureSuccessStatusCode();
var order = await _db.Orders.FirstAsync();
order.Status.Should().Be("Created");
}
}
Testing Anti-Patterns
- Mocking Hell: Коли ви мокаєте репозиторій, щоб протестувати сервіс, який просто викликає репозиторій. Ви тестуєте мок, а не код.
- Testing Implementation Details: Тести падають при кожному рефакторингу приватних методів. Тестуйте поведінку (Public API), а не реалізацію.
5. Decision Tree: Вибір Сховища Даних
Ще одне часте питання: SQL чи NoSQL?
- Relational (Postgres, MySQL): Дефолтний вибір. ACID, стабільність, зв'язки. Підходить для 90% задач.
- Document (Mongo, DynamoDB):
- Агрегати добре лягають на документи (один JSON).
- Гнучка схема (Generic Subdomain, CMS).
- Ризик: Втрата цілісності даних при слабких транзакціях (хоча сучасні NoSQL вже мають ACID).
- Graph (Neo4j): Соціальні мережі, рекомендаційні системи, Fraud Detection (пошук зв'язків).
- Time-Series (TimescaleDB, Influx): IoT, метрики, фінансові тікери.
Евристика Поліглота
6. Евристики Рефакторингу
Коли і як змінювати дизайн?
Strangler Fig (Фіга-Душитель)
Якщо у вас є Моноліт, не переписуйте його "з нуля" (Big Bang Rewrite). Це майже завжди закінчується провалом. Використовуйте патерн Strangler Fig:
- Поставте проксі (nginx/YARP) перед монолітом.
- Напишіть нову функціональність у новому мікросервісі.
- Перенаправте трафік (
/api/v2/orders) на новий сервіс. - Поступово переносьте стару функціональність ("душіть" моноліт).
Rule of Three (Правило Трьох)
Не виділяйте спільний код (Shared Kernel/Library), поки він не повторився у трьох різних місцях. Виділення абстракцій надто рано (Premature Abstraction) призводить до створенная універсальних монстрів, які важко підтримувати. Краще мати трохи дублювання (Duplication is cheaper than wrong abstraction).
6. Висновки
Евристики — це не догми. Це стартові точки для вашого мислення.
- Always start simple. Почніть з Модульного Моноліту, Шаруватої Архітектури і Postgres.
- Refactor slightly later. Вводьте DDD, CQRS, Hexagonal тільки тоді, коли біль від поточної архітектури стає нестерпним.
- Align with Business. Технічні рішення повинні відображати бізнес-цілі (Subdomains).
У наступній, фінальній главі, ми розглянемо, як ці рішення еволюціонують у часі. Що робити, коли Core Domain стає Legacy, і як проводити рефакторинг великих систем.
Глава 9. Патерни Взаємодії
У попередніх розділах (глави 5–8) ми фокусувалися на тактичних патернах — як будувати окремі компоненти системи, моделювати бізнес-логіку всередині одного Обмеженого Контексту (Bounded Context). Ми навчилися створювати Агрегати, Об'єкти-Значення та організовувати їх у Шарувату або Гексагональну архітектуру.
Глава 11. Еволюція Проектних Рішень
Вітаю! Якщо ви читаєте ці рядки, ви пройшли довгий шлях від визначення Єдиної Мови до реалізації складних Саг і Агрегатів.