"It depends" (Це залежить) — найпопулярніша відповідь будь-якого консультанта чи архітектора. Але від чого саме це залежить?
Коли використовувати CQRS, а коли вистачить звичайного CRUD? Коли будувати складну Гексагональну Архітектуру, а коли просто писати код в контролерах? Коли мікросервіси — це благо, а коли — смерть проекту?
Ця глава — це набір Евристик (емпіричних правил), які допоможуть вам приймати зважені рішення. Ми перетворимо абстрактне "It depends" на конкретні алгоритми прийняття рішень.
Межі Контекстів
Бізнес-Логіка
Архітектура
Одна з найчастіших помилок в DDD — нарізання системи на надто дрібні шматки (Entity Services) або створення одного гігантського моноліту.
Не діліть контексти по сутностях (CustomerService, OrderService, ProductService).
Діліть по Бізнес-Можливостях (Business Capabilities) або Поведінці.
ProductService: зберігає id, name, price, stock.CatalogContext: пошук, категорії, SEO-атрибути.InventoryContext: залишки на складах, резервування, інвентаризація.PricingContext: знижки, промо-акції, динамічне ціноутворення.Контекст повинен бути таким, щоб його могла підтримувати одна команда (Two-Pizza Team, 5-9 людей). Якщо над контекстом працює 20 людей — це забагато когнітивного навантаження. Розбивайте його. Якщо над 5-ма контекстами працює одна людина — це оверхед на перемикання контексту та інфраструктуру. Об'єднуйте їх.
Якщо частина системи змінюється щодня (напр. фронтенд промо-акцій), а інша частина — раз на рік (напр. ядро бухгалтерського обліку), їх варто розділити. Різні темпи змін вимагають різних процесів релізу і тестування.
Згідно з книгою Team Topologies, межі сервісів повинні збігатися з межами когнітивного навантаження команди. Якщо команда "тоне" в підтримці, постійно перемикається між контекстами і боїться чіпати старий код — це сигнал, що межі контекстів прокладені неправильно.
Евристика Розміру
Контекст не є "завеликим", якщо:
Не вся логіка однакова. Для "Core Domain" ми хочемо максимум гнучкості, для "Generic Subdomain" — купити готове або зробити швидко.
Вибирайте патерн реалізації в залежності від складності піддомену.
| Патерн | Складність Домену | Складність Структур Даних | Приклад |
|---|---|---|---|
| Transaction Script | Низька | Низька | Адмінка, прості звіти, ETL джоби |
| Active Record | Низька/Середня | Середня | Прості CRUD додатки, блоги |
| Domain Model | Висока | Висока | Core Domain, складна бізнес-логіка, інваріанти |
| Event Sourced | Дуже Висока | Часова (Temporal) | Бухгалтерія, Медичні картки, Git-подібні системи |
Давайте порівняємо код.
Логіка розмазана по сервісу. Сутності тупі (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).
Логіка інкапсульована в Агрегаті. Сервіс тільки координує.
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);
}
}
Перевага: Логіка тестується юніт-тестами без бази даних. Правила живуть поруч з даними.
Мартін Фаулер називає Анемічну Модель (сутності тільки з геттерами/сеттерами) анти-патерном. Але в DDD це залежить від контексту.
Архітектура — це про структурування коду.
Підходить для більшості проектів на старті.
Для систем, які живуть довго і мають складну бізнес-логіку.
Використовувати тільки точково!
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
Як тестувати DDD-системи? Забудьте про "Test Pyramid" у її класичному розумінні (багато юнітів, мало інтеграційних), якщо ви робите мікросервіси.
У мікросервісах одиницею ізоляції є Сервіс, а не клас. Тому Integration Tests стають найважливішими.
Найкращий 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");
}
}
Ще одне часте питання: SQL чи NoSQL?
Евристика Поліглота
Коли і як змінювати дизайн?
Якщо у вас є Моноліт, не переписуйте його "з нуля" (Big Bang Rewrite). Це майже завжди закінчується провалом. Використовуйте патерн Strangler Fig:
/api/v2/orders) на новий сервіс.Не виділяйте спільний код (Shared Kernel/Library), поки він не повторився у трьох різних місцях. Виділення абстракцій надто рано (Premature Abstraction) призводить до створенная універсальних монстрів, які важко підтримувати. Краще мати трохи дублювання (Duplication is cheaper than wrong abstraction).
Евристики — це не догми. Це стартові точки для вашого мислення.
У наступній, фінальній главі, ми розглянемо, як ці рішення еволюціонують у часі. Що робити, коли Core Domain стає Legacy, і як проводити рефакторинг великих систем.
Глава 9. Патерни Взаємодії
У попередніх розділах (глави 5–8) ми фокусувалися на тактичних патернах — як будувати окремі компоненти системи, моделювати бізнес-логіку всередині одного Обмеженого Контексту (Bounded Context). Ми навчилися створювати Агрегати, Об'єкти-Значення та організовувати їх у Шарувату або Гексагональну архітектуру.
Глава 11. Еволюція Проектних Рішень
Вітаю! Якщо ви читаєте ці рядки, ви пройшли довгий шлях від визначення Єдиної Мови до реалізації складних Саг і Агрегатів.