Domain-Driven Design, layers, repositories, services, dependency injection
По умолчанию Django поощряет «толстые view» и «модели-на-всё»: бизнес-логика расползается по view, формам, сигналам и методам моделей. На маленьком проекте это нормально. Но когда логика становится сложной, такой код тяжело тестировать (всё завязано на ORM и HTTP), менять и понимать. DDD и Clean Architecture отвечают на вопрос: как изолировать бизнес-правила от фреймворка.
💡 Правило: архитектурные слои — это инвестиция, которая окупается на сложной доменной логике (биллинг, страхование, логистика) и становится оверинжинирингом на простом CRUD. Не вводите слои «потому что правильно» — вводите, когда чувствуете боль от их отсутствия.
Главный принцип — правило зависимостей: зависимости направлены только внутрь, к домену. Внешние слои знают о внутренних, но не наоборот. Домен не знает ни про Django, ни про БД, ни про HTTP.
┌─────────────────────────────────────────────┐
│ interfaces/ views, DRF, CLI, GraphQL │ ← снаружи
│ ┌───────────────────────────────────────┐ │
│ │ application/ use cases, services │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ domain/ сущности, правила │ │ │ ← внутри (чистый Python)
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
│ infrastructure/ Django ORM, API, очереди │
└─────────────────────────────────────────────┘
зависимости направлены ВНУТРЬ →
| Слой | Ответственность | Можно импортировать |
|---|---|---|
domain/ | Сущности и бизнес-правила. Чистый Python | Ничего внешнего |
application/ | Use cases: оркестрация сценариев | domain |
infrastructure/ | ORM, внешние API, очереди, кэш | domain, application |
interfaces/ | Views, сериализаторы, CLI | application |
⚠️ Признак нарушения архитектуры —
from django.db import modelsв слоеdomain. Как только домен зависит от ORM, вы не можете тестировать бизнес-правила без БД и не можете сменить хранилище.
Репозиторий прячет способ хранения за интерфейсом. Application-слой работает с «коллекцией пользователей», не зная, что под капотом Django ORM (а завтра — другой источник). Это делает бизнес-логику тестируемой через подмену репозитория in-memory заглушкой.
# domain/repositories.py — контракт, без реализации
from typing import Protocol
from .entities import User
class UserRepository(Protocol):
def get_by_id(self, user_id: int) -> User | None: ...
def get_by_email(self, email: str) -> User | None: ...
def save(self, user: User) -> None: ...# infrastructure/repositories.py — конкретная реализация на Django ORM
from myapp.models import UserORM
from domain.entities import User
class DjangoUserRepository:
def get_by_id(self, user_id: int) -> User | None:
row = UserORM.objects.filter(pk=user_id).first()
return self._to_entity(row) if row else None
def save(self, user: User) -> None:
UserORM.objects.update_or_create(
pk=user.id,
defaults={'email': user.email, 'name': user.name},
)
def _to_entity(self, row) -> User:
# ORM-модель → доменная сущность (домен не видит ORM)
return User(id=row.id, email=row.email, name=row.name)💡 Ключевой приём — маппинг между ORM-моделью (
UserORM) и доменной сущностью (User). Они намеренно разные: ORM-модель про хранение, сущность про поведение и правила. Многие команды на Django считают это избыточным и держат сущность = модель — это допустимый компромисс для проектов средней сложности.
Service/use case — это один бизнес-сценарий целиком: оркестрация репозиториев, правил и побочных эффектов. Сюда переезжает логика из «толстых view». View становится тонким: распарсил вход → вызвал use case → отдал результат.
# application/services.py
from dataclasses import dataclass
@dataclass
class PlaceOrderService:
orders: OrderRepository
products: ProductRepository
notifier: Notifier # зависимости инжектятся, а не создаются внутри
def execute(self, user_id: int, product_ids: list[int]) -> Order:
products = [self.products.get_by_id(p) for p in product_ids]
order = Order.create(user_id=user_id, products=products) # доменное правило
order.validate() # бизнес-валидация
self.orders.save(order)
self.notifier.order_placed(order)
return order# interfaces/views.py — view тонкий, не знает деталей домена
def place_order(request):
service = PlaceOrderService(
orders=DjangoOrderRepository(),
products=DjangoProductRepository(),
notifier=EmailNotifier(),
)
order = service.execute(request.user.id, request.POST.getlist('products'))
return JsonResponse({'order_id': order.id})💡 Dependency Injection (передача зависимостей снаружи, а не создание внутри) — то, что делает use case тестируемым: в тесте вы подставляете
InMemoryOrderRepositoryиFakeNotifierи проверяете бизнес-логику без БД и почты.
Доменная сущность хранит и данные, и инварианты — правила, которые обязаны выполняться всегда. Логику держат внутри сущности, а не размазывают по сервисам («rich domain model» против «anemic model»).
# domain/entities.py — чистый Python, ноль Django
from dataclasses import dataclass, field
@dataclass
class Order:
id: int | None
user_id: int
items: list = field(default_factory=list)
status: str = 'draft'
def add_item(self, product, qty: int):
if self.status != 'draft':
raise DomainError('Нельзя менять оформленный заказ') # инвариант
self.items.append((product, qty))
@property
def total(self) -> int:
return sum(p.price * q for p, q in self.items)
def place(self):
if not self.items:
raise DomainError('Пустой заказ нельзя оформить')
self.status = 'placed'| Признак | Вывод |
|---|---|
| Сложная доменная логика, много правил и инвариантов | ✅ Слои окупятся |
| Логику тяжело тестировать из-за привязки к ORM/HTTP | ✅ Repository + DI помогут |
| Долгоживущий проект, большая команда | ✅ Границы спасают от хаоса |
| Простой CRUD, прототип, MVP | ❌ Оверинжиниринг, замедлит разработку |
| Маленькая команда, простой домен | ❌ «Толстые модели» Django достаточно |
⚠️ Самая частая ошибка — натянуть полный DDD на CRUD-приложение. Получается втрое больше кода без выигрыша. Начинайте с идиоматичного Django (service-функции + менеджеры моделей) и вводите слои точечно там, где сложность реально болит.
domain свободным от импортов Django/ORMВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Микросервисы с Django