DDD
DDD⚑
Основні принципи DDD⚑
Domain-Driven Design (DDD) — це підхід до проєктування складних програмних систем, спрямований на чітке вираження бізнес-логіки та її ізоляцію від технічних деталей. Головна ідея DDD полягає в тому, щоб модель програми максимально відповідала реальним бізнес-процесам і була зрозумілою як для розробників, так і для експертів предметної області.
DDD складається з двох основних рівнів:
- Стратегічний рівень
- Bounded Contexts — визначає межі системи та контексти, в яких певні терміни мають специфічне значення
- Ubiquitous Language — формує єдину мову для ефективної взаємодії з бізнес-експертами та між командами
- Context Mapping — керує відносинами між різними контекстами та їх інтеграцією
- Тактичний рівень - описує конкретні шаблони реалізації всередині bounded context
- Entities — сутності з унікальною ідентичністю
- Aggregates — кластери пов'язаних об'єктів
- Value Objects — об'єкти без ідентичності
- Domain Services — сервіси для бізнес-логіки, що не належить конкретній сутності
Уся бізнес-логіка зосереджена в доменному шарі, а саме в сутностях, агрегатах та Value Objects. Тут визначаються:
- Бізнес-інваріанти (правила, що завжди мають виконуватися)
- Допустимі переходи станів
- Валідні операції
Зміна стану та перевірка інваріантів відбуваються виключно через методи сутності, які явно виражають бізнес-зміст: complete()
, cancel()
, change_owner()
тощо.
Доменні моделі не залежать від інфраструктури (ані від баз даних, ані від транспорту, ані від зовнішніх DTO), і це дозволяє повторно використовувати бізнес-логіку незалежно від того, які технології використовуються в інфраструктурному шарі. Репозиторії, адаптери, транспорт, логування, моніторинг та інша інфраструктура виносяться за межі домену й використовуються тільки в application-шарі.
Сервіси-оркестратори - це тонкий шар, який координує дії між доменними об'єктами та інфраструктурою. Він сам не містить бізнес-логіки, але керує викликами доменних методів, збором даних і збереженням результатів. Він також єдиний шар, який може спілкуватися з зовнішніми системами й взаємодіяти з репозиторіями, надсилати події, логувати тощо.
Переваги використання DDD:
- Чітке вираження бізнес-логіки — код відображає реальні бізнес-процеси
- Покращена комунікація — спільна мова з бізнес-експертами
- Модульність — чіткі межі між компонентами
- Тестованість — легко тестувати бізнес-логіку ізольовано
- Еволюційність — система легко адаптується до змін у бізнесі
- Повторне використання — доменна логіка не залежить від технологій
Домен⚑
Доменна модель — це сукупність структур, які виражають бізнес-правила та поведінку предметної області.
У широкому сенсі це не один конкретний тип, а сукупність:
- Entity
- Aggregate
- Value Object (VO)
- Domain Service.
Entity (сутність) - об'єкт предметної області з ідентичністю (ID) і життєвим циклом. Він може мати стан, бізнес-методи й брати участь в агрегатах.
Ознаки:
- Має ID, за яким визначається унікальність.
- Може мати змінюваний стан.
- Інкапсулює бізнес-інваріанти.
- Може бути частиною агрегату або його коренем (Aggregate Root).
Сутність і її інваріанти
# domain/tasks/entity.py
class Task:
def __init__(self, id: str, status: str, owner_id: str):
self.id = id
self.status = status
self.owner_id = owner_id
def complete(self):
if self.status != "in_progress":
raise ValueError("Invalid status transition")
return Task(self.id, "completed", self.owner_id)
def assign_to(self, new_owner_id: str):
if self.status == "completed":
raise ValueError("Unable to change the owner of a completed task")
return Task(self.id, self.status, new_owner_id)
Value Object (VO) - це об'єкт, який не має ідентичності й визначається виключно своїми значеннями. Він іммутабельний і використовується для представлення концепцій, не будучи сутністю. Дозволяють повторно використовувати бізнес-логіку та не дублювати валідації.
Ознаки:
- Немає ID. Об'єкти вважаються рівними, якщо у них однакові значення.
- Іммутабельність. Після створення не змінюється.
- Інкапсулює валідацію. Перевіряє коректність значень на етапі створення.
- Не зберігається окремо. Не має власних таблиць/репозиторіїв.
# domain/value_objects.py
class Email:
def __init__(self, value: str):
if not self._is_valid_email(value):
raise ValueError("Invalid email")
self.value = value
def _is_valid_email(self, email: str) -> bool:
return "@" in email and "." in email.split("@")[1]
def __eq__(self, other):
return isinstance(other, Email) and self.value == other.value
def __str__(self):
return self.value
Агрегат - це кластер сутностей і Value Objects, які логічно пов'язані й управляються як єдине ціле. Агрегат визначає межі консистентності й вхідну точку для операцій над пов'язаними об'єктами, допомагають контролювати стан складних об'єктів через єдиний вхід (aggregate root).
Ознаки:
- Має Aggregate Root - головну сутність для доступу до агрегату. Тільки через неї здійснюється доступ до інших даних.
- Гарантія інваріантів. Будь-яка операція зберігає внутрішню узгодженість.
- Визначає межі транзакції. Все всередині агрегату змінюється в рамках однієї транзакції.
- Не розкриває вкладені сутності назовні напряму. Зовнішній доступ тільки через Aggregate Root.
# domain/orders/aggregate.py
class Order: # Aggregate Root
def __init__(self, id: str, customer_id: str):
self.id = id
self.customer_id = customer_id
self.items = []
self.status = "draft"
def add_item(self, product_id: str, quantity: int, price: Money):
if self.status != "draft":
raise ValueError("Unable to add product to inactive order")
item = OrderItem(product_id, quantity, price)
self.items.append(item)
def confirm(self):
if not self.items:
raise ValueError("Unable to confirm empty order")
self.status = "confirmed"
Domain Service - це компонент доменного шару, який інкапсулює бізнес-логіку, що не належить конкретній сутності або агрегату, але все ще є частиною предметної області.
Використовується, коли логіка:
- неприродно лягає на одну конкретну сутність
- потребує координації кількох об'єктів
- при цьому залишається всередині одного домену.
Ознаки:
- Містить бізнес-логіку
- Не має власного стану (stateless)
- Не є сутністю чи Value Object
- Працює з кількома доменними моделями
- Не залежить від інфраструктури
# domain/services/pricing_service.py
class PricingService:
def calculate_discount(self, customer: Customer, order: Order) -> Money:
discount_rate = 0.0
if customer.is_vip():
discount_rate += 0.1
if order.total_amount() > Money(1000, "USD"):
discount_rate += 0.05
return order.total_amount() * discount_rate
Application Service
# application/task_service.py
class TaskService:
def __init__(self, task_repo: TaskRepository, event_publisher: EventPublisher):
self.task_repo = task_repo
self.event_publisher = event_publisher
def complete_task(self, task_id: str, user_id: str):
task = self.task_repo.get_by_id(task_id) # Get data
if task.owner_id != user_id: # Check access rights
raise ValueError("Insufficient rights to complete task")
completed_task = task.complete() # Call domain logic
self.task_repo.save(completed_task) # Save the result
self.event_publisher.publish(TaskCompletedEvent(completed_task.id)) # Publish the event
Domain Service vs Application Service
- Domain Service - операція стосується однієї сутності або агрегату.
- Application Service - потрібна координація кількох доменів або інфраструктурних компонентів.
Допоміжні елементи DDD⚑
View/Read Model - це проекція доменної моделі, призначена тільки для читання, часто агрегована під конкретний use-case. Вона оптимізує читання даних, розвантажує доменну модель і дозволяє безпечно відображати агреговану інформацію.
Ознаки:
- Не є частиною домену.
- Не містить бізнес-логіки. Якщо у View з'являється поведінка, то розмивається межа між Domain Model і View Model, що порушує SRP. Виняток: методи, які не змінюють стан і не виконують бізнес-логіку, наприклад
__str__()
,format()
. - Використовується для відображення, звітів, API-відповідей тощо.
- Може містити агреговані дані з кількох джерел.
# views/task_view.py
class TaskSummaryView:
def __init__(self, id: str, title: str, status: str, owner_name: str, created_at: datetime):
self.id = id
self.title = title
self.status = status
self.owner_name = owner_name
self.created_at = created_at
def format_for_display(self) -> str:
return f"{self.title} ({self.status}) - {self.owner_name}"
Data Transfer Object (DTO) - тимчасова структура, що використовується для передачі даних між шарами, наприклад, між транспортом і сервісом. DTO забезпечують слабкий зв'язок між шарами й сервісами, дозволяють ізолювати зміни й формувати API-контракти.
Ознаки:
- Не має поведінки й бізнес-логіки.
- Використовується в транспортному шарі (gRPC, HTTP), мапиться до/з Entity/VO через конвертери. Ізолює зовнішній API від внутрішніх моделей.
- Може бути двостороннім (RequestDTO ↔ ResponseDTO).
# dto/task_dto.py
class CreateTaskRequest:
def __init__(self, title: str, description: str, owner_id: str):
self.title = title
self.description = description
self.owner_id = owner_id
class TaskResponse:
def __init__(self, id: str, title: str, status: str, owner_id: str):
self.id = id
self.title = title
self.status = status
self.owner_id = owner_id