Разработка, ориентированная на предметную область
Eric Evans, 2003
Domain-Driven Design — подход к разработке сложных систем, при котором основное внимание уделяется предметной области (домену), а не техническим деталям.
# ❌ Код не отражает реальность
class UserTable:
def __init__(self):
self.id = None
self.name = None
self.email = None
# Что такое "UserTable"? Это таблица? Или пользователь?
Код говорит на языке БД, а не на языке бизнеса
# ✅ Код отражает реальность
class User:
def __init__(self, name: str, email: Email):
self.name = name
self.email = email
def change_email(self, new_email: Email):
if not new_email.is_valid():
raise ValueError("Invalid email")
self.email = new_email
Код говорит на языке бизнеса
Объект с уникальным идентификатором. Две сущности равны, если их ID равны.
class User: # Entity
def __init__(self, user_id: str, name: str):
self.id = user_id # Уникальный идентификатор
self.name = name
def __eq__(self, other):
return isinstance(other, User) and self.id == other.id
# Два пользователя с одинаковым ID — одна сущность
user1 = User("123", "Alice")
user2 = User("123", "Bob") # Другой name, но тот же ID
assert user1 == user2 # True!
Объект без идентификатора. Равенство по значению всех полей.
class Email: # Value Object
def __init__(self, address: str):
if "@" not in address:
raise ValueError("Invalid email")
self.address = address.lower()
def __eq__(self, other):
return isinstance(other, Email) and self.address == other.address
email1 = Email("user@example.com")
email2 = Email("USER@EXAMPLE.COM") # Разный регистр
assert email1 == email2 # True! Значение одинаковое
💡 Value Objects неизменяемы (immutable)
Группа связанных объектов, которые рассматриваются как единое целое.
class Order: # Aggregate Root
def __init__(self, order_id: str, customer: Customer):
self.id = order_id
self.customer = customer
self.items = [] # List[OrderItem]
self.status = "pending"
def add_item(self, product: Product, quantity: int):
if self.status != "pending":
raise ValueError("Cannot modify completed order")
item = OrderItem(product, quantity)
self.items.append(item)
def calculate_total(self):
return sum(item.price * item.quantity for item in self.items)
# OrderItem — часть агрегата Order
class OrderItem:
def __init__(self, product: Product, quantity: int):
self.product = product
self.quantity = quantity
🔹 Агрегат защищает свою целостность
Абстракция для работы с хранилищем. Скрывает детали БД.
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def save(self, user: User) -> None:
pass
@abstractmethod
def find_by_id(self, user_id: str) -> User:
pass
@abstractmethod
def find_by_email(self, email: Email) -> User:
pass
# Реализация для PostgreSQL
class PostgresUserRepository(UserRepository):
def __init__(self, db):
self.db = db
def save(self, user: User):
self.db.execute(
"INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
(user.id, user.name, user.email.address)
)
def find_by_id(self, user_id: str) -> User:
row = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
return User(row['id'], row['name'], Email(row['email']))
# Реализация для MongoDB
class MongoUserRepository(UserRepository):
def save(self, user: User):
self.collection.insert_one({
"_id": user.id,
"name": user.name,
"email": user.email.address
})
Бизнес-логика, которая не принадлежит конкретной Entity.
class TransferService: # Domain Service
def __init__(self, account_repo: AccountRepository):
self.account_repo = account_repo
def transfer(self, from_id: str, to_id: str, amount: Money):
from_account = self.account_repo.find_by_id(from_id)
to_account = self.account_repo.find_by_id(to_id)
# Логика перевода не принадлежит Account
if from_account.balance < amount:
raise ValueError("Insufficient funds")
from_account.withdraw(amount)
to_account.deposit(amount)
self.account_repo.save(from_account)
self.account_repo.save(to_account)
💡 Когда логика не "вписывается" в Entity → Domain Service
Границы, внутри которых модель имеет одно значение.
┌─────────────────────┐ ┌─────────────────────┐ │ E-Commerce Context │ │ Shipping Context │ │ │ │ │ │ User: покупатель │ │ User: получатель │ │ - name │ │ - address │ │ - email │ │ - phone │ │ - cart │ │ - delivery_prefs │ └─────────────────────┘ └─────────────────────┘ Одна сущность "User" имеет разное значение в разных контекстах!
┌─────────────────────────────────┐ │ Presentation Layer │ ← UI, API ├─────────────────────────────────┤ │ Application Layer │ ← Use Cases, координация ├─────────────────────────────────┤ │ Domain Layer │ ← Entity, Value Objects, Domain Services ├─────────────────────────────────┤ │ Infrastructure Layer │ ← DB, HTTP, Email └─────────────────────────────────┘
ecommerce/
├── domain/ # Domain Layer
│ ├── entities/
│ │ ├── user.py
│ │ ├── order.py
│ │ └── product.py
│ ├── value_objects/
│ │ ├── email.py
│ │ ├── money.py
│ │ └── address.py
│ ├── services/
│ │ └── transfer_service.py
│ └── repositories/
│ └── user_repository.py
│
├── application/ # Application Layer
│ ├── usecases/
│ │ ├── create_user.py
│ │ ├── place_order.py
│ │ └── transfer_money.py
│ └── dto/
│ └── user_dto.py
│
├── infrastructure/ # Infrastructure Layer
│ ├── persistence/
│ │ └── postgres_user_repository.py
│ ├── email/
│ │ └── smtp_email_service.py
│ └── api/
│ └── fastapi_app.py
│
└── presentation/ # Presentation Layer
└── api/
└── routes.py
# application/usecases/create_user.py
class CreateUserUseCase:
def __init__(self, user_repo: UserRepository, email_service: EmailService):
self.user_repo = user_repo
self.email_service = email_service
def execute(self, name: str, email: str) -> UserDTO:
# 1. Валидация
email_obj = Email(email)
# 2. Проверка существования
if self.user_repo.find_by_email(email_obj):
raise ValueError("User already exists")
# 3. Создание сущности
user = User(id=generate_id(), name=name, email=email_obj)
# 4. Сохранение
self.user_repo.save(user)
# 5. Отправка приветственного email
self.email_service.send_welcome(user.email)
# 6. Возврат DTO
return UserDTO.from_entity(user)
Моделируем домен для ML-платформы
# domain/entities/training_job.py
class TrainingJob: # Aggregate Root
def __init__(self, job_id: str, dataset: Dataset, model_config: ModelConfig):
self.id = job_id
self.dataset = dataset
self.config = model_config
self.status = JobStatus.PENDING
self.metrics = Metrics()
self.created_at = datetime.now()
def start(self):
if self.status != JobStatus.PENDING:
raise ValueError("Job already started")
self.status = JobStatus.RUNNING
def complete(self, metrics: Metrics):
if self.status != JobStatus.RUNNING:
raise ValueError("Job not running")
self.status = JobStatus.COMPLETED
self.metrics = metrics
def fail(self, error: str):
self.status = JobStatus.FAILED
self.error_message = error
# domain/value_objects/metrics.py
class Metrics: # Value Object
def __init__(self, accuracy: float, loss: float, f1_score: float):
if not (0 <= accuracy <= 1):
raise ValueError("Accuracy must be between 0 and 1")
self.accuracy = accuracy
self.loss = loss
self.f1_score = f1_score
def __eq__(self, other):
return (
isinstance(other, Metrics) and
self.accuracy == other.accuracy and
self.loss == other.loss and
self.f1_score == other.f1_score
)
def is_better_than(self, other: Metrics) -> bool:
return self.accuracy > other.accuracy
# domain/repositories/training_job_repository.py
from abc import ABC, abstractmethod
class TrainingJobRepository(ABC):
@abstractmethod
def save(self, job: TrainingJob) -> None:
pass
@abstractmethod
def find_by_id(self, job_id: str) -> TrainingJob:
pass
@abstractmethod
def find_by_status(self, status: JobStatus) -> List[TrainingJob]:
pass
# application/usecases/start_training.py
class StartTrainingUseCase:
def __init__(
self,
job_repo: TrainingJobRepository,
dataset_repo: DatasetRepository,
trainer: ModelTrainer
):
self.job_repo = job_repo
self.dataset_repo = dataset_repo
self.trainer = trainer
def execute(self, job_id: str):
# 1. Получаем job
job = self.job_repo.find_by_id(job_id)
# 2. Запускаем job
job.start()
self.job_repo.save(job)
# 3. Запускаем обучение (async)
self.trainer.train_async(job, self._on_complete)
def _on_complete(self, job_id: str, metrics: Metrics):
job = self.job_repo.find_by_id(job_id)
job.complete(metrics)
self.job_repo.save(job)
# infrastructure/persistence/postgres_training_job_repository.py
class PostgresTrainingJobRepository(TrainingJobRepository):
def __init__(self, db):
self.db = db
def save(self, job: TrainingJob):
self.db.execute("""
INSERT INTO training_jobs
(id, dataset_id, config, status, metrics, created_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status,
metrics = EXCLUDED.metrics
""", (
job.id,
job.dataset.id,
json.dumps(job.config.to_dict()),
job.status.value,
json.dumps(job.metrics.to_dict()) if job.metrics else None,
job.created_at
))
def find_by_id(self, job_id: str) -> TrainingJob:
row = self.db.fetch_one(
"SELECT * FROM training_jobs WHERE id = ?",
(job_id,)
)
return self._to_entity(row)
def _to_entity(self, row) -> TrainingJob:
# Преобразование из БД в Entity
dataset = self.dataset_repo.find_by_id(row['dataset_id'])
config = ModelConfig.from_dict(json.loads(row['config']))
job = TrainingJob(row['id'], dataset, config)
job.status = JobStatus(row['status'])
if row['metrics']:
job.metrics = Metrics.from_dict(json.loads(row['metrics']))
return job
# presentation/api/routes.py
from fastapi import FastAPI, Depends
from application.usecases.start_training import StartTrainingUseCase
app = FastAPI()
@app.post("/training-jobs/{job_id}/start")
def start_training(
job_id: str,
use_case: StartTrainingUseCase = Depends(get_start_training_use_case)
):
try:
use_case.execute(job_id)
return {"status": "started"}
except ValueError as e:
return {"error": str(e)}, 400
| Подход | Когда использовать |
|---|---|
| CRUD | Простые приложения, админки |
| Transaction Script | Простая бизнес-логика, скрипты |
| Active Record | Быстрая разработка, Django |
| DDD | Сложная бизнес-логика, долгосрочные проекты |
# ❌ Модель без поведения
class User:
def __init__(self):
self.id = None
self.name = None
self.email = None
# Вся логика в сервисах
class UserService:
def validate_user(self, user: User):
return "@" in user.email
def change_email(self, user: User, new_email: str):
user.email = new_email
Модель — просто "мешок с данными"
# ✅ Модель с поведением
class User:
def __init__(self, user_id: str, name: str, email: Email):
self.id = user_id
self.name = name
self.email = email
def change_email(self, new_email: Email):
if not new_email.is_valid():
raise ValueError("Invalid email")
self.email = new_email
def is_valid(self) -> bool:
return self.email.is_valid() and len(self.name) > 0
Логика рядом с данными
# ❌ Домен зависит от БД
class User:
def save(self):
db.execute("INSERT INTO users ...") # Инфраструктура!
# ✅ Домен чистый
class User:
def change_email(self, new_email: Email):
self.email = new_email
# Репозиторий в инфраструктуре
class UserRepository:
def save(self, user: User):
db.execute("INSERT INTO users ...")
"DDD — это не про код. Это про мышление.
Это про то, как понимать проблему и выражать решение."