🎙️ Вступ
Цей текст містить нотатки, взяті з випуску "Domain Driven Design: Tactical Patterns", який був опублікований на каналі Trendyol Tech 9 травня 2024 року. Відео доступне за посиланням: Domain Driven Design: Tactical Patterns.
⚠️ Чому ці патерни?
Domain-Driven Design (DDD) є важливим посібником для правильного моделювання складних бізнес-правил. В тактичній частині DDD розглядаються патерни, які допомагають вам правильно спроектувати доменну модель.
Особливо важливо зрозуміти різницю між Anemic Model і Rich Model.
📊 Що таке Anemic Model?
- Тільки переносить дані.
- Бізнес-правила та валідації зберігаються окремо (зазвичай у сервісах).
- Порушує інкапсуляцію.
- Протирічить принципу єдиної відповідальності.
- Бізнес-логіка просочується в шар сервісів, що ускладнює підтримку та розуміння.
✅ Що таке Rich Domain Model?
- Дані та поведінка об’єднані разом.
- Забезпечує інкапсуляцію.
- Об’єкт контролює свою власну валідність.
- Шар сервісів не потребує знань про внутрішні деталі моделі.
- Покращена читаємість, підтримка та розширюваність.
💎 Що таке Value Object?
- Не має ідентичності (без ID).
- Не змінний (immutable), не змінюється після створення.
- Самостійно перевіряє свою валідність.
- Два value object з однаковими значеннями є рівними.
- Приклад: валюта, відстань, номер телефону.
📦 Що таке Entity?
- Має ідентичність (ID).
- Відслідковується через ідентичність.
- Може бути змінним (mutable).
- Дві entity з однаковими значеннями, але різними ідентичностями — це різні об’єкти.
🌐 Що таке Aggregate & Aggregate Root?
- Сукупність, яка містить кілька entity та value object.
- Aggregate Root керує взаємодією з зовнішнім світом.
- Прямий доступ до entity поза Aggregate Root неможливий.
- Завантажується та зберігається атомарно (atomicity).
- Гарантує консистентність даних.
- Використовує версіонування (Optimistic Locking) для запобігання одночасним змінам.
⚒️ Ефективний дизайн Aggregate
- Великі кластерні агрегати: Містять багато entity, високий ризик конфліктів.
- Багато малих агрегатів: Надмірне дроблення ускладнює управління.
- Оптимальні агрегати: Охоплюють правильні інваріанти, є достатньо широкими, але не надмірними.
- Зв’язок між агрегатами повинен відбуватися лише через ID.
- Можуть підтримуватися структурами типу Eventually Consistent.
- Рекомендується принцип "Один агрегат на транзакцію".
📡 Domain Events
- Повідомляють зовнішній світ про важливі події, що сталися в Aggregate.
- Забезпечують слабку зв’язність між агрегатами.
- Обробники подій розташовуються на рівні додатку.
- Payload події містить лише необхідну інформацію, без додаткових даних, специфічних для споживача.
- Використання Outbox Pattern запобігає втраті подій.
🏭 Factory
- Відповідає за створення Aggregate, entity або value object.
- Охоплює складні сценарії створення.
- Може керувати інтеграцією з зовнішніми системами.
- Розділяє логіку створення моделі та бізнес-логіку.
🛠️ Domain Service
- Сервіси, які працюють з кількома entity або value object.
- Не зберігають стан.
- Зазвичай використовуються для порівняння, агрегації та обробки між entity.
- Якщо Rich Model достатньо, потреба в domain service зменшується.
- Необхідно уникати ризику Anemic Model.
📑 CQRS (Command Query Responsibility Segregation)
- Command: Змінює дані (insert, update, delete).
- Query: Зчитує дані (read).
- Розділення запитів на читання та запис забезпечує кращу продуктивність та масштабованість.
- Сумісність з Eventually Consistent моделями.
- Command та Query Handler виконують одну конкретну задачу.
🔗 Event Sourcing
- Зберігає не фінальний стан, а всі події.
- За допомогою Projector створюється read model (останній стан) на основі подій.
- Усі зміни можна відслідковувати.
- Структура "Append-Only" забезпечує перевагу в продуктивності.
- Управління Event Store, версіонування та схема еволюції викликають додаткові труднощі.
⚖️ Separation of Concerns
- Рекомендується використовувати маленькі command та handler замість великих сервісів.
- Легко тестується.
- Управління помилками та транзакціями може бути поділене на події.
- Кроки можуть бути модульно організовані за допомогою Chain of Responsibility або Pipeline Patterns.
📐 Hexagonal Architecture (Ports & Adapters)
- Запропоновано Алістером Кокберном.
- Мета: відокремити внутрішню частину застосунку від зовнішніх залежностей.
- Внутрішнє ядро (Core): містить Domain та Application Logic.
- Порти: інтерфейси, через які застосунок взаємодіє із зовнішнім світом.
- Адаптер: Описує, як порти взаємодіють з зовнішнім світом (БД, API, UI тощо).
UI / Rest / CLI
|
Адаптер
|
Порт
|
Сервіс додатку
|
Домени
- ✅ Незалежність тестування
- ✅ Мінімальна залежність від технологій
- ✅ Чіткі межі та відповідальність
🔗 Розподілені транзакції
- У розподілених системах управління транзакціями, що охоплюють кілька сервісів і джерел даних, є складною задачею.
- Гарантування ACID стає важким.
- Шляхи вирішення:
- ✅ Two-Phase Commit (2PC)
- ✅ Saga Pattern
- ✅ Outbox Pattern + Eventual Consistency
- ✅ Компенсаційні транзакції (зворотні операції)
✅ Two-Phase Commit (2PC)
- Класичний підхід до управління розподіленими транзакціями.
- Transaction Coordinator управляє всіма учасниками.
- Етапи:
1.
Prepare Phase: Усі учасники отримують запитання "Чи готові ви?".
2. Commit Phase: Якщо всі учасники відповідають "Готовий", виконується commit. Якщо виникає помилка, виконується rollback.
Переваги
✅ Забезпечує гарантії ACID.
Недоліки
❌ Проблеми з продуктивністю (високий час очікування).
❌ Залежність від однієї точки (Transaction Coordinator).
❌ Не рекомендується у мікросервісному світі.
🔄 Saga Pattern
- Великий процес розділяється на ланцюг локальних транзакцій, що виконуються послідовно.
- Кожен крок виконує свою локальну транзакцію та генерує подію.
- Наступний сервіс, який слухає цю подію, запускає свою локальну транзакцію.
Приклад процесу
- Служба замовлень створює замовлення (локальна транзакція).
- Служба замовлень відправляє подію "Отримати оплату" до служби платежів.
- Служба платежів отримує оплату (локальна транзакція), а потім надсилає подію до служби доставки.
- Служба доставки ініціює доставку (локальна транзакція).
Переваги
- ✅ Гнучкість та масштабованість.
- ✅ Підходить для Event-Driven систем.
Недоліки
- ❌ Проблеми з eventual consistency.
- ❌ Управління помилками та механізми повтору можуть бути складними.
- ❌ Низька спостережуваність (distributed tracing) стає критичною.
Порівняння
🌟 Підсумок рекомендацій
- Для монолітних систем або операцій, що взаємодіють з одним джерелом даних: 2PC.
- Для мікросервісів та асинхронних процесів: Saga Pattern (переважно Choreography).
- Якщо критичний досвід користувача та необхідність у швидкому зворотному зв'язку: Outbox + Eventual Consistency.
🔥 Останні нотатки та приклади
- Кожен domain event повинен відображати лише один стан.
- Слід уникати event’ів, які містять дані, специфічні для споживача.
- Domain event’и не повинні надсилатися напряму в інші bounded context (анти-патерн).
- Domain event’и не повинні губитися, має використовуватися Outbox Pattern.
- Транзакційні межі повинні визначатися на основі агрегатів.
- Якщо в одній транзакції змінюється кілька агрегатів, це зазвичай є помилкою проектування.
Перекладено з: 📝 DDD Tactical Patterns Notları