Разделение бизнес-логики и данных в Django: лучшие практики
Как правильно разделить бизнес-логику и доступ к данным в Django ORM. Сервисный слой, толстые модели, QuerySet методы, CQRS и чистая архитектура для улучшения django архитектуры и избежания смешивания ответственности.
Как правильно разделить бизнес-логику и доступ к данным в Django приложениях? Какие лучшие практики существуют для разделения сущностей базы данных и бизнес-логики, чтобы избежать смешивания ответственности и улучшить архитектуру приложения?
Разделение бизнес-логики и доступа к данным в Django является одним из ключевых аспектов создания масштабируемых и поддерживаемых приложений. Правильная архитектура позволяет избежать смешивания ответственности и делает код более читаемым, тестируемым и гибким к изменениям. Основные подходы включают использование сервисного слоя, менеджеров и методов QuerySet, а также следование принципам чистой и шестигранной архитектуры.
Содержание
- Введение в разделение бизнес-логики и доступа к данным в Django
- Основные архитектурные паттерны для разделения ответственности
- Паттерн “толстые модели” в Django: преимущества и недостатки
- Сервисный слой: организация бизнес-логики вне моделей
- Методы QuerySet и кастомные менеджеры для сложных запросов
- Команды vs Запросы: подход к разделению действий и вопросов
- Чистая архитектура и шестигранная архитектура в Django
- Практические примеры и лучшие практики
Введение в разделение бизнес-логики и доступа к данным в Django
В мире веб-разработки Django ORM предоставляет мощные инструменты для работы с базами данных, но важно понимать разницу между моделью данных и доменной моделью. Модель данных отвечает за то, что мы храним в базе данных, в то время как доменная модель содержит бизнес-логику и сущности, воспринимаемые конечным пользователем.
При работе с django orm возникает естественное желание вынести всю логику в модели, но это может привести к созданию “анти-паттерна”, где модели становятся слишком большими и сложно поддерживаемыми. Правильное разделение помогает создать чистую архитектуру, где каждый компонент имеет четкую ответственность.
Ключевая проблема, которую решает такое разделение — это предотвращение смешивания ответственности. Когда бизнес-логика и доступ к данным смешиваются в одном месте, код становится трудно переиспользовать, тестировать и поддерживать. Разделение же позволяет создавать модульные компоненты, которые можно изменять независимо друг от друга.
Основные архитектурные паттерны для разделения ответственности
Существует несколько основных паттернов для разделения бизнес-логики и доступа к данным в Django. Каждый из них имеет свои преимущества и области применения:
Паттерн “толстые модели” (Fat Models)
Этот подход предполагает размещение большей части бизнес-логики непосредственно в моделях Django. Он особенно хорош для операций, связанных с одним объектом:
class Order(models.Model):
# Поля модели
created_at = models.DateTimeField(auto_now_add=True)
total = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20)
def calculate_discount(self):
"""Бизнес-логика, специфичная для одного заказа"""
if self.status == 'VIP':
return self.total * Decimal('0.1')
return Decimal('0')
def is_ready_for_shipping(self):
"""Логика проверки готовности к отправке"""
return self.status == 'PROCESSING' and self.total > 0
Преимущества этого подхода:
- Логика находится рядом с данными
- Естественное использование внутри представления
- Хорошо подходит для простых приложений
Недостатки:
- Модели могут стать слишком большими
- Трудно переиспользовать логику в разных контекстах
- Сложность тестирования
Сервисный слой (Service Layer)
Сервисный слой представляет собой отдельный модуль (обычно services.py), который находится между моделями и представлениями. Он централизует бизнес-логику, особенно для операций, охватывающих несколько моделей:
# services.py
class OrderService:
@staticmethod
def create_order_with_items(user, items_data):
"""Создание заказа с товарами"""
order = Order.objects.create(user=user, status='PROCESSING')
for item_data in items_data:
OrderItem.objects.create(
order=order,
product=item_data['product'],
quantity=item_data['quantity'],
price=item_data['price']
)
return order
@staticmethod
def calculate_statistics_for_period(start_date, end_date):
"""Расчет статистики за период"""
orders = Order.objects.filter(
created_at__range=[start_date, end_date]
)
return {
'total_orders': orders.count(),
'total_revenue': orders.aggregate(
total=models.Sum('total')
)['total'] or 0
}
Преимущества:
- Четкое разделение ответственности
- Легкое переиспользование
- Удобство тестирования
- Централизация сложной логики
Менеджеры и QuerySet методы
Для сложных запросов и операций над множеством объектов можно использовать кастомные менеджеры и методы QuerySet:
class OrderManager(models.Manager):
def get_vip_orders(self):
"""Получение VIP заказов"""
return self.filter(status='VIP')
def get_recent_orders(self, days=7):
"""Получение недавних заказов"""
from django.utils import timezone
return self.filter(
created_at__gte=timezone.now() - timezone.timedelta(days=days)
)
class Order(models.Model):
# Поля модели
# ...
objects = OrderManager()
@classmethod
def get_average_order_value(cls):
"""Классовый метод для сложных запросов"""
return cls.objects.aggregate(
avg_value=models.Avg('total')
)['avg_value'] or 0
Этот подход особенно полезен для часто используемых шаблонов запросов и операций, связанных с конкретной моделью.
Паттерн “толстые модели” в Django: преимущества и недостатки
Паттерн “толстых моделей” (Fat Models) — один из самых популярных подходов в Django сообществе. Его суть заключается в том, чтобы размещать большую часть бизнес-логики непосредственно в классах моделей Django.
Когда использовать “толстые модели”
Этот подход хорошо работает в следующих сценариях:
- Логика, специфичная для одного объекта — методы, которые работают с конкретным экземпляром модели:
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField()
is_active = models.BooleanField(default=True)
def is_available(self):
"""Проверка доступности товара"""
return self.is_active and self.stock > 0
def apply_discount(self, discount_percent):
"""Применение скидки к цене"""
if not 0 <= discount_percent <= 100:
raise ValueError("Скидка должна быть в диапазоне 0-100")
self.price = self.price * (1 - discount_percent / 100)
self.save()
- Хуки модели — использование методов
save(),delete()и сигналов для выполнения связанных операций:
class Order(models.Model):
# Поля
status = models.CharField(max_length=20, default='NEW')
def save(self, *args, **kwargs):
"""Переопределение save для бизнес-логики"""
if self.status == 'CANCELLED' and self.pk:
# Отмена заказа - логика обработки
self.cancel_order_items()
super().save(*args, **kwargs)
def cancel_order_items(self):
"""Отмена позиций заказа"""
OrderItem.objects.filter(order=self).update(
status='CANCELLED'
)
Преимущества паттерна “толстые модели”
-
Естественная организация — логика находится рядом с данными, что делает код интуитивно понятным.
-
Простота использования — методы модели можно вызывать прямо из представлений без дополнительных импортов.
-
Инкапсуляция — бизнес-правила скрыты внутри модели, что предотвращает их неправильное использование.
-
Автоматическое тестирование — методы модели легко тестировать независимо от представлений.
Недостатки и ограничения
-
Проблемы масштабирования — в крупных проектах модели могут стать слишком большими и сложными для понимания.
-
Сложность тестирования — когда методы модели зависят от других моделей или внешних сервисов, их тестирование становится сложным.
-
Смешивание слоев — нарушение принципа единственной ответственности, особенно когда методы модели начинают работать с несколькими другими моделями.
-
Проблемы с переиспользованием — бизнес-логика, привязанная к модели, сложно переиспользуется в других контекстах.
Когда стоит избегать “толстых моделей”
Этот подход не рекомендуется в следующих случаях:
- Когда бизнес-логика охватывает несколько моделей
- Когда логика требует сложных внешних зависимостей
- При работе с асинхронными операциями
- В системах с высокой нагрузкой, где модели могут стать узким местом
Как отмечает James Bennett, основный разработчик Django, не стоит создавать отдельный слой сервисов для операций, которые ORM уже хорошо поддерживает. Вместо этого лучше использовать встроенные возможности Django, такие как методы менеджера и QuerySet.
Сервисный слой: организация бизнес-логики вне моделей
Сервисный слой — это один из самых мощных подходов для организации бизнес-логики в Django приложениях. Он представляет собой отдельный модуль, который находится между моделями и представлениями и отвечает за выполнение сложных операций, особенно тех, которые охватывают несколько моделей.
Что такое сервисный слой
Сервисный слой — это набор классов или функций, инкапсулирующих бизнес-логику приложения. В отличие от методов модели, сервисы могут:
- Работать с несколькими моделями одновременно
- Использовать внешние API и сервисы
- Управлять транзакциями и сложными операциями
- Предоставлять единый интерфейс для сложных бизнес-процессов
Пример структуры проекта с сервисным слоем:
myapp/
├── models.py
├── services.py # Сервисный слой
├── views.py
├── tests/
│ ├── test_models.py
│ ├── test_services.py
│ └── test_views.py
└── ...
Создание сервисного слоя
Вот пример сервисного слоя для интернет-магазина:
# services.py
from django.db import transaction
from decimal import Decimal
class OrderService:
@staticmethod
def create_order_with_items(user, items_data):
"""Создание заказа с товарами"""
with transaction.atomic():
order = Order.objects.create(
user=user,
status='PROCESSING'
)
total = Decimal('0')
for item_data in items_data:
product = Product.objects.get(id=item_data['product_id'])
quantity = item_data['quantity']
# Проверка наличия
if product.stock < quantity:
raise ValueError(f"Недостаточно товара: {product.name}")
# Создание позиции заказа
order_item = OrderItem.objects.create(
order=order,
product=product,
quantity=quantity,
price=product.price
)
total += product.price * quantity
# Уменьшение остатка
product.stock -= quantity
product.save()
# Обновление итоговой суммы
order.total = total
order.save()
return order
@staticmethod
def cancel_order(order_id, reason=None):
"""Отмена заказа"""
try:
with transaction.atomic():
order = Order.objects.get(id=order_id)
if order.status == 'CANCELLED':
raise ValueError("Заказ уже отменен")
order.status = 'CANCELLED'
order.cancellation_reason = reason
order.save()
# Возврат остатков
for item in order.items.all():
item.product.stock += item.quantity
item.product.save()
return order
except Order.DoesNotExist:
raise ValueError("Заказ не найден")
@staticmethod
def get_user_statistics(user):
"""Получение статистики пользователя"""
return {
'total_orders': Order.objects.filter(user=user).count(),
'total_spent': Order.objects.filter(
user=user
).aggregate(
total=models.Sum('total')
)['total'] or 0,
'favorite_category': OrderItem.objects.filter(
order__user=user
).values_list('product__category').annotate(
count=models.Count('id')
).order_by('-count').first()
}
Преимущества сервисного слоя
-
Четкое разделение ответственности — сервисы отвечают только за бизнес-логику, модели — только за данные.
-
Легкое тестирование — сервисы можно тестировать независимо от представлений и моделей.
-
Переиспользование — один и тот же сервис можно использовать в представлениях, API, задачах и командах управления.
-
Централизация сложной логики — все сложные операции находятся в одном месте, что упрощает поддержку.
-
Управление транзакциями — сервисы могут легко управлять сложными транзакционными операциями.
Интеграция с представлениями
Сервисы легко интегрируются с представлениями Django:
# views.py
from django.shortcuts import render, redirect
from .services import OrderService
def create_order_view(request):
if request.method == 'POST':
try:
items_data = [
{'product_id': int(pid), 'quantity': int(qty)}
for pid, qty in request.POST.getlist('product_id', [])
]
order = OrderService.create_order_with_items(
request.user,
items_data
)
return redirect('order_detail', order.id)
except ValueError as e:
# Обработка ошибок
return render(request, 'order/create.html', {
'error': str(e),
'items_data': request.POST
})
return render(request, 'order/create.html')
def cancel_order_view(request, order_id):
if request.method == 'POST':
try:
OrderService.cancel_order(
order_id,
reason=request.POST.get('reason')
)
return redirect('my_orders')
except ValueError as e:
return render(request, 'order/cancel.html', {
'error': str(e)
})
Сервисы и сигналы
Сервисы могут работать вместе с сигналами Django для обработки асинхронных операций:
# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .services import OrderService
from .models import Order
@receiver(post_save, sender=Order)
def handle_order_creation(sender, instance, created, **kwargs):
"""Обработка создания заказа"""
if created:
# Асинхронная обработка
try:
OrderService.send_order_notification(instance)
except Exception:
# Логирование ошибки, но не прерывание транзакции
pass
Когда использовать сервисный слой
Сервисный слой особенно полезен в следующих случаях:
- Когда бизнес-логика охватывает несколько моделей
- Для сложных транзакционных операций
- При работе с внешними API и сервисами
- Для операций, которые могут выполняться в разных контекстах (представления, API, задачи)
- Когда нужна централизация сложной логики
Как отмечает команда PyPy Django, сервисный слой естественным образом возникает при использовании класс-объектных представлений (CBV) — бизнес-логика выносится в сервисы, а представления остаются “тонкими”.
Методы QuerySet и кастомные менеджеры для сложных запросов
Методы QuerySet и кастомные менеджеры — это мощные инструменты Django ORM, которые позволяют инкапсулировать сложные запросы и операции над множествами объектов. Они предоставляют элегантный способ организации бизнес-логики, связанной с извлечением и обработкой данных.
Кастомные менеджеры в Django
Кастомные менеджеры позволяют определить специализированные методы для работы с запросами к модели. Они особенно полезны для часто используемых шаблонов запросов:
class ProductManager(models.Manager):
def get_available_products(self):
"""Получение доступных товаров"""
return self.filter(
stock__gt=0,
is_active=True
)
def get_products_by_category(self, category_id):
"""Получение товаров по категории"""
return self.filter(
category_id=category_id,
is_active=True
).select_related('category')
def get_search_products(self, query):
"""Поиск товаров"""
return self.filter(
models.Q(name__icontains=query) |
models.Q(description__icontains=query),
is_active=True
)
class Product(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField()
is_active = models.BooleanField(default=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE)
objects = ProductManager()
Использование кастомного менеджера:
# Получение доступных товаров
available_products = Product.objects.get_available_products()
# Поиск товаров
search_results = Product.objects.get_search_products("iphone")
# Товары по категории
category_products = Product.objects.get_products_by_category(1)
Методы QuerySet
Методы QuerySet позволяют добавлять сложную логику непосредственно в класс модели. Они полезны для операций, которые могут быть переиспользованы в нескольких местах:
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
total = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
class Meta:
ordering = ['-created_at']
@classmethod
def get_user_orders(cls, user):
"""Получение заказов пользователя с предзагрузкой связанных данных"""
return cls.objects.filter(user=user).select_related('user')
@classmethod
def get_recent_orders(cls, days=30):
"""Получение недавних заказов"""
from django.utils import timezone
return cls.objects.filter(
created_at__gte=timezone.now() - timezone.timedelta(days=days)
)
@classmethod
def get_orders_by_status(cls, status):
"""Получение заказов по статусу"""
return cls.objects.filter(status=status)
def get_order_items_total(self):
"""Получение суммы позиций заказа"""
return self.items.aggregate(
total=models.Sum(
models.F('quantity') * models.F('price')
)
)['total'] or 0
Комбинация менеджеров и методов QuerySet
Наиболее мощным подходом является сочетание кастомных менеджеров и методов QuerySet:
class OrderManager(models.Manager):
def get_vip_orders(self):
"""Получение VIP заказов"""
return self.filter(
user__is_vip=True,
status__in=['PROCESSING', 'SHIPPED']
)
def get_orders_with_total_above(self, amount):
"""Получение заказов с суммой выше указанной"""
return self.annotate(
items_total=models.Sum(
models.F('items__quantity') * models.F('items__price')
)
).filter(items_total__gt=amount)
class Order(models.Model):
# Поля модели
# ...
objects = OrderManager()
@classmethod
def get_monthly_statistics(cls):
"""Статистика за месяц"""
from django.db.models import Avg, Count, Sum
from django.utils import timezone
from django.db.models.functions import TruncMonth
return cls.objects.annotate(
month=TruncMonth('created_at')
).values('month').annotate(
order_count=Count('id'),
avg_total=Avg('total'),
total_revenue=Sum('total')
).order_by('month')
def get_shipping_address(self):
"""Получение адреса доставки"""
return self.addresses.filter(
is_shipping=True
).first()
Продвинутые техники с QuerySet
Для более сложных сценариев можно использовать аннотации, агрегации и сложные фильтры:
class ProductManager(models.Manager):
def get_category_statistics(self):
"""Статистика по категориям"""
return self.values('category__name').annotate(
product_count=Count('id'),
avg_price=Avg('price'),
total_stock=Sum('stock')
).order_by('-product_count')
def get_products_with_discount(self):
"""Товары со скидкой"""
from django.db.models import F, ExpressionWrapper, DecimalField
return self.annotate(
discounted_price=ExpressionWrapper(
F('price') * Decimal('0.8'),
output_field=DecimalField()
)
).filter(discounted_price__lt=F('price'))
def get_similar_products(self, product_id, limit=5):
"""Похожие товары"""
target_product = self.get(id=product_id)
return self.filter(
category=target_product.category,
id__ne=product_id
).exclude(id=product_id)[:limit]
Использование методов QuerySet в сервисах и представлениях
Методы QuerySet легко интегрируются с сервисным слоем и представлениями:
# services.py
class ProductService:
@staticmethod
def get_popular_products(limit=10):
"""Получение популярных товаров"""
return Product.objects.get_popular_products()[:limit]
@staticmethod
def get_products_for_recommendations(user):
"""Получение товаров для рекомендаций"""
bought_categories = OrderItem.objects.filter(
order__user=user
).values_list('product__category', flat=True)
return Product.objects.filter(
category__in=bought_categories
).distinct()[:20]
# views.py
def product_list_view(request):
"""Список товаров"""
category_id = request.GET.get('category')
if category_id:
products = Product.objects.get_products_by_category(category_id)
else:
products = Product.objects.get_available_products()
return render(request, 'products/list.html', {
'products': products
})
Преимущества использования методов QuerySet и менеджеров
-
Переиспользование — сложные запросы можно использовать в разных частях приложения без дублирования кода.
-
Инкапсуляция — логика запросов скрыта внутри модели или менеджера, что предотвращает ошибки.
-
Читаемость — код становится более читаемым благодаря абстрактным методам вместо сложных запросов.
-
Тестируемость — методы QuerySet легко тестируются независимо от бизнес-логики.
-
Производительность — предзагрузка связанных данных и оптимизация запросов улучшают производительность.
Как отмечает publysher на Stack Overflow, методы QuerySet являются естественным местом для размещения логики извлечения данных, в то время как бизнес-логика операций лучше размещать в сервисах или формах.
Команды vs Запросы: подход к разделению действий и вопросов
Разделение команд и запросов (CQRS - Command Query Responsibility Segregation) — это мощный архитектурный паттерн, который помогает четко разделить операции чтения и записи в приложении. В контексте Django этот подход позволяет создавать более чистую и масштабируемую архитектуру.
Основная идея CQRS
CQRS разделяет операции на две категории:
- Команды (Commands) — операции, изменяющие состояние системы (создание, обновление, удаление)
- Запросы (Queries) — операции, только считывающие данные без их изменения
Такое разделение позволяет оптимизировать каждую категорцию операций независимо друг от друга.
Реализация CQRS в Django
Команды
Команды представляют собой объекты или функции, инкапсулирующие операции изменения данных:
# commands.py
from dataclasses import dataclass
from django.db import transaction
@dataclass
class CreateOrderCommand:
"""Команда создания заказа"""
user_id: int
items_data: list
shipping_address: dict
def execute(self):
"""Выполнение команды"""
from .models import Order, OrderItem, Product
with transaction.atomic():
user = User.objects.get(id=self.user_id)
order = Order.objects.create(
user=user,
status='PROCESSING'
)
total = 0
for item_data in self.items_data:
product = Product.objects.get(id=item_data['product_id'])
quantity = item_data['quantity']
# Проверка и обновление остатка
if product.stock < quantity:
raise ValueError(f"Недостаточно товара: {product.name}")
product.stock -= quantity
product.save()
# Создание позиции заказа
OrderItem.objects.create(
order=order,
product=product,
quantity=quantity,
price=product.price
)
total += product.price * quantity
order.total = total
order.save()
return order
@dataclass
class UpdateUserAddressCommand:
"""Команда обновления адреса пользователя"""
user_id: int
address_data: dict
def execute(self):
"""Выполнение команды"""
user = User.objects.get(id=self.user_id)
# Обновление или создание адреса
address, created = Address.objects.update_or_create(
user=user,
is_default=True,
defaults=self.address_data
)
return address
Запросы
Запросы отвечают только за чтение данных и могут быть оптимизированы для производительности:
# queries.py
from dataclasses import dataclass
from django.db.models import Prefetch
@dataclass
class GetUserOrdersQuery:
"""Запрос получения заказов пользователя"""
user_id: int
page: int = 1
per_page: int = 10
def execute(self):
"""Выполнение запроса"""
from .models import Order
offset = (self.page - 1) * self.per_page
orders = Order.objects.filter(
user_id=self.user_id
).select_related('user').prefetch_related(
Prefetch(
'items',
queryset=OrderItem.objects.select_related('product')
)
)[offset:offset + self.per_page]
return orders
@dataclass
class GetProductStatisticsQuery:
"""Запрос статистики по товарам"""
category_id: int = None
days: int = 30
def execute(self):
"""Выполнение запроса"""
from django.utils import timezone
from .models import OrderItem, Product
queryset = OrderItem.objects.filter(
order__created_at__gte=timezone.now() - timezone.timedelta(days=self.days)
)
if self.category_id:
queryset = queryset.filter(product__category_id=self.category_id)
return queryset.values('product__name').annotate(
sold_quantity=models.Sum('quantity'),
total_revenue=models.Sum(
models.F('quantity') * models.F('price')
)
).order_by('-total_revenue')
Использование команд и запросов в представлениях
# views.py
from django.shortcuts import render, redirect
from .commands import CreateOrderCommand, UpdateUserAddressCommand
from .queries import GetUserOrdersQuery, GetProductStatisticsQuery
def create_order_view(request):
"""Создание заказа"""
if request.method == 'POST':
try:
command = CreateOrderCommand(
user_id=request.user.id,
items_data=request.POST.getlist('items'),
shipping_address=request.POST.dict()
)
order = command.execute()
return redirect('order_detail', order.id)
except ValueError as e:
return render(request, 'order/create.html', {
'error': str(e)
})
return render(request, 'order/create.html')
def user_orders_view(request):
"""Заказы пользователя"""
query = GetUserOrdersQuery(
user_id=request.user.id,
page=request.GET.get('page', 1)
)
orders = query.execute()
return render(request, 'orders/list.html', {
'orders': orders
})
def admin_statistics_view(request):
"""Статистика для администратора"""
category_id = request.GET.get('category')
days = int(request.GET.get('days', 30))
query = GetProductStatisticsQuery(
category_id=category_id,
days=days
)
statistics = query.execute()
return render(request, 'admin/statistics.html', {
'statistics': statistics,
'category_id': category_id,
'days': days
})
Преимущества CQRS подхода
-
Оптимизация производительности — запросы можно оптимизировать независимо от команд, используя разные модели хранения, кэширование и индексацию.
-
Четкое разделение ответственности — команды и запросы имеют разные цели и могут изменяться независимо.
-
Масштабируемость — чтение и запись могут масштабироваться независимо друг от друга.
-
Упрощение тестирования — команды и запросы легко тестируются изолированно.
-
Гибкость — можно использовать разные модели для чтения и записи данных.
Ограничения CQRS
-
Сложность — добавляет дополнительный уровень сложности в архитектуру.
-
Дублирование кода — некоторые операции могут дублироваться в командах и запросах.
-
Не всегда необходимо — для простых приложений CQRS может быть избыточным.
Когда использовать CQRS
CQRS особенно полезен в следующих сценариях:
- Приложения с высокой нагрузкой на чтение
- Системы, где чтение и запись имеют разные требования к производительности
- Комплексные бизнес-процессы с четким разделением операций
- Системы, требующие масштабирования по горизонтали
Как отмечает команда GeeksforGeeks, CQRS является естественным развитием сервисного подхода и особенно эффективен в крупных Django проектах с высокой нагрузкой.
Чистая архитектура и шестигранная архитектура в Django
Чистая архитектура (Clean Architecture) и шестигранная архитектура (Hexagonal Architecture) — это современные подходы к проектированию программного обеспечения, которые позволяют создавать гибкие, тестируемые и независимые от инфраструктуры приложения. В контексте Django эти подходы помогают четко разделить бизнес-логику от доступа к данным и других внешних зависимостей.
Основные принципы чистой архитектуры
Чистая архитектура, предложенная Робертом К. Мартином, основана на следующих принципах:
-
Зависимости направлены внутрь — все зависимости направлены от внешних слоев к внутренним, а не наоборот.
-
Используйте интерфейсы — для взаимодействия между слоями используются абстракции (интерфейсы).
-
Бизнес-логика независима от инфраструктуры — ядро приложения не зависит от базы данных, веб-фреймворков, внешних API и т.д.
-
Высокая связность и низкоеcoupling — каждый слой имеет четную ответственность и слабо связан с другими слоями.
Шестигранная архитектура (Hexagonal Architecture)
Шестигранная архитектура, также известная как портов и адаптеров, предлагает аналогичный подход с акцентом на:
-
Центральная бизнес-логика — основная логика приложения находится в центре.
-
Порты — интерфейсы, через которые бизнес-логика взаимодействует с внешним миром.
-
Адаптеры — реализации портов, которые подключаются к различным внешним системам (базы данных, веб-API, файловая система и т.д.).
Реализация чистой архитектуры в Django
Структура проекта
Структура проекта с чистой архитектурой может выглядеть следующим образом:
myproject/
├── core/ # Ядро приложения (бизнес-логика)
│ ├── domain/
│ │ ├── entities/
│ │ ├── repositories/
│ │ └── services/
│ └── application/
│ ├── use_cases/
│ └── dtos/
├── infrastructure/ # Инфраструктурный слой
│ ├── django/
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ └── ...
│ ├── repositories/
│ │ └── django_repository.py
│ └── external_apis/
└── presentation/ # Слой представления
├── web/
│ ├── views/
│ ├── templates/
│ └── ...
└── api/
└── views.py
Доменные сущности
Доменные сущности представляют собой бизнес-объекты без привязки к конкретной технологии:
# core/domain/entities/order.py
from dataclasses import dataclass
from decimal import Decimal
from datetime import datetime
@dataclass
class Order:
"""Заказ - доменная сущность"""
id: int
user_id: int
items: list
total: Decimal
status: str
created_at: datetime
def calculate_total(self) -> Decimal:
"""Расчет суммы заказа"""
return sum(item.price * item.quantity for item in self.items)
def can_be_cancelled(self) -> bool:
"""Проверка возможности отмены"""
return self.status in ['NEW', 'PROCESSING']
def apply_discount(self, discount_percent: Decimal) -> None:
"""Применение скидки"""
if not 0 <= discount_percent <= 100:
raise ValueError("Скидка должна быть в диапазоне 0-100")
discount_factor = Decimal('1') - (discount_percent / Decimal('100'))
self.total = self.total * discount_factor
@dataclass
class OrderItem:
"""Позиция заказа - доменная сущность"""
product_id: int
name: str
price: Decimal
quantity: int
@property
def total(self) -> Decimal:
"""Общая сумма позиции"""
return self.price * self.quantity
Репозитории
Репозитории определяют интерфейсы для доступа к данным:
# core/domain/repositories/order_repository.py
from abc import ABC, abstractmethod
from typing import List, Optional
from ..entities.order import Order, OrderItem
class OrderRepository(ABC):
"""Интерфейс репозитория заказов"""
@abstractmethod
def save(self, order: Order) -> Order:
"""Сохранение заказа"""
pass
@abstractmethod
def get_by_id(self, order_id: int) -> Optional[Order]:
"""Получение заказа по ID"""
pass
@abstractmethod
def get_by_user_id(self, user_id: int) -> List[Order]:
"""Получение заказов пользователя"""
pass
@abstractmethod
def update_status(self, order_id: int, status: str) -> None:
"""Обновление статуса заказа"""
pass
class ProductRepository(ABC):
"""Интерфейс репозитория продуктов"""
@abstractmethod
def get_by_id(self, product_id: int):
"""Получение продукта по ID"""
pass
@abstractmethod
def get_available_products(self):
"""Получение доступных продуктов"""
pass
Use Cases (Применения)
Use Cases представляют собой бизнес-процессы:
# core/application/use_cases/order_use_cases.py
from typing import List
from ...domain.entities.order import Order
from ...domain.repositories.order_repository import OrderRepository
from ...domain.repositories.product_repository import ProductRepository
class CreateOrderUseCase:
"""UseCase создания заказа"""
def __init__(
self,
order_repository: OrderRepository,
product_repository: ProductRepository
):
self.order_repository = order_repository
self.product_repository = product_repository
def execute(self, user_id: int, item_ids: List[int]) -> Order:
"""Выполнение создания заказа"""
# Получение продуктов
products = []
for product_id in item_ids:
product = self.product_repository.get_by_id(product_id)
if not product:
raise ValueError(f"Продукт с ID {product_id} не найден")
products.append(product)
# Создание доменной сущности заказа
order = Order(
id=None, # Будет сгенерировано при сохранении
user_id=user_id,
items=products,
total=0, # Будет рассчитано
status='NEW',
created_at=datetime.now()
)
# Расчет суммы
order.total = order.calculate_total()
# Сохранение
return self.order_repository.save(order)
class CancelOrderUseCase:
"""UseCase отмены заказа"""
def __init__(self, order_repository: OrderRepository):
self.order_repository = order_repository
def execute(self, order_id: int) -> None:
"""Отмена заказа"""
order = self.order_repository.get_by_id(order_id)
if not order:
raise ValueError("Заказ не найден")
if not order.can_be_cancelled():
raise ValueError("Заказ нельзя отменить")
# Возврат остатков (опущено для краткости)
self.order_repository.update_status(order_id, 'CANCELLED')
Инфраструктурный слой
Инфраструктурный слой реализует интерфейсы репозиториев с использованием Django:
# infrastructure/repositories/django_order_repository.py
from typing import List, Optional
from django.db import transaction
from ...core.domain.entities.order import Order, OrderItem
from ...core.domain.repositories.order_repository import OrderRepository
from ..django.models import Order as DjangoOrder, OrderItem as DjangoOrderItem
class DjangoOrderRepository(OrderRepository):
"""Реализация репозитория заказов на Django"""
def save(self, order: Order) -> Order:
"""Сохранение заказа"""
with transaction.atomic():
# Преобразование доменной сущности в Django модель
django_order = DjangoOrder.objects.create(
user_id=order.user_id,
total=order.total,
status=order.status,
created_at=order.created_at
)
# Сохранение позиций
for item in order.items:
DjangoOrderItem.objects.create(
order=django_order,
product_id=item.product_id,
name=item.name,
price=item.price,
quantity=item.quantity
)
# Обновление ID в доменной сущности
order.id = django_order.id
return order
def get_by_id(self, order_id: int) -> Optional[Order]:
"""Получение заказа по ID"""
try:
django_order = DjangoOrder.objects.select_related(
'user'
).prefetch_related(
'items__product'
).get(id=order_id)
# Преобразование в доменную сущность
items = [
OrderItem(
product_id=item.product_id,
name=item.product.name,
price=item.price,
quantity=item.quantity
)
for item in django_order.items.all()
]
return Order(
id=django_order.id,
user_id=django_order.user_id,
items=items,
total=django_order.total,
status=django_order.status,
created_at=django_order.created_at
)
except DjangoOrder.DoesNotExist:
return None
def get_by_user_id(self, user_id: int) -> List[Order]:
"""Получение заказов пользователя"""
django_orders = DjangoOrder.objects.filter(
user_id=user_id
).select_related('user').prefetch_related('items__product')
orders = []
for django_order in django_orders:
items = [
OrderItem(
product_id=item.product_id,
name=item.product.name,
price=item.price,
quantity=item.quantity
)
for item in django_order.items.all()
]
orders.append(Order(
id=django_order.id,
user_id=django_order.user_id,
items=items,
total=django_order.total,
status=django_order.status,
created_at=django_order.created_at
))
return orders
def update_status(self, order_id: int, status: str) -> None:
"""Обновление статуса заказа"""
DjangoOrder.objects.filter(id=order_id).update(status=status)
Представления
Представления остаются “тонкими” и только вызывают use cases:
# presentation/web/views/order_views.py
from django.shortcuts import render, redirect
from ...core.application.use_cases.order_use_cases import (
CreateOrderUseCase,
CancelOrderUseCase
)
from ...infrastructure.repositories.django_order_repository import (
DjangoOrderRepository
)
from ...infrastructure.repositories.django_product_repository import (
DjangoProductRepository
)
def create_order_view(request):
"""Создание заказа"""
if request.method == 'POST':
try:
# Создание use case
use_case = CreateOrderUseCase(
DjangoOrderRepository(),
DjangoProductRepository()
)
# Выполнение
order = use_case.execute(
user_id=request.user.id,
item_ids=request.POST.getlist('product_id')
)
return redirect('order_detail', order.id)
except ValueError as e:
return render(request, 'order/create.html', {
'error': str(e)
})
return render(request, 'order/create.html')
def cancel_order_view(request, order_id):
"""Отмена заказа"""
if request.method == 'POST':
try:
use_case = CancelOrderUseCase(DjangoOrderRepository())
use_case.execute(order_id)
return redirect('my_orders')
except ValueError as e:
return render(request, 'order/cancel.html', {
'error': str(e)
})
return render(request, 'order/cancel.html')
Преимущества чистой и шестигранной архитектуры
-
Тестируемость — бизнес-логика может тестироваться без зависимости от Django или базы данных.
-
Масштабируемость — легко заменять компоненты (например, заменить Django на FastAPI).
-
Четкое разделение ответственности — каждый слой имеет четную роль.
-
Гибкость — легко добавлять новые технологии или изменять существующие.
-
Поддерживаемость — код становится более понятным и легким для изменения.
Ограничения и сложности
-
Сложность реализации — чистая архитектура добавляет много boilerplate кода.
-
Избыточность для простых проектов — для небольших приложений такой подход может быть избыточным.
-
Кривая обучения — требует понимания архитектурных паттернов.
Когда использовать чистую архитектуру
Чистая архитектура особенно полезна в следующих сценариях:
- Крупные корпоративные приложения с долгим жизненным циклом
- Системы, которые могут потребовать миграции на другую технологию
- Проекты с большим количеством бизнес-логики
- Команды, работающие над разными частями приложения параллельно
Как отмечает wsvincent на Django Forum, чистая архитектура является естественным развитием сервисного подхода и особенно эффективна в сложных Django проектах, где требуется высокая гибкость и тестируемость.
Практические примеры и лучшие практики
В этом разделе мы рассмотрим практические примеры реализации различных архитектурных подходов в Django, а также лучшие практики для разделения бизнес-логики и доступа к данным.
Пример 1: Интернет-магазин с сервисным слоем
Рассмотрим комплексный пример интернет-магазина с использованием сервисного слоя.
Модели
# models.py
from django.db import models
from django.conf import settings
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
class Order(models.Model):
STATUS_CHOICES = [
('NEW', 'Новый'),
('PROCESSING', 'В обработке'),
('SHIPPED', 'Отправлен'),
('DELIVERED', 'Доставлен'),
('CANCELLED', 'Отменен'),
]
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='NEW')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
shipping_address = models.TextField()
total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
def __str__(self):
return f"Заказ #{self.id} - {self.user.username}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"{self.quantity} x {self.product.name}"
@property
def total(self):
return self.quantity * self.price
Сервисный слой
# services.py
from django.db import transaction
from decimal import Decimal
from .models import Order, OrderItem, Product
class OrderService:
"""Сервис для работы с заказами"""
@staticmethod
@transaction.atomic
def create_order(user, items_data, shipping_address):
"""Создание заказа"""
order = Order.objects.create(
user=user,
shipping_address=shipping_address,
status='NEW'
)
total = Decimal('0')
for item_data in items_data:
product = Product.objects.get(
id=item_data['product_id'],
is_active=True
)
if product.stock < item_data['quantity']:
raise ValueError(f"Недостаточно товара: {product.name}")
# Создание позиции заказа
order_item = OrderItem.objects.create(
order=order,
product=product,
quantity=item_data['quantity'],
price=product.price
)
# Обновление остатка
product.stock -= item_data['quantity']
product.save()
total += order_item.total
# Обновление итоговой суммы
order.total = total
order.save()
return order
@staticmethod
@transaction.atomic
def cancel_order(order_id, reason=None):
"""Отмена заказа"""
try:
order = Order.objects.get(id=order_id, status__in=['NEW', 'PROCESSING'])
# Возврат остатков
for item in order.items.all():
item.product.stock += item.quantity
item.product.save()
# Обновление статуса
order.status = 'CANCELLED'
if reason:
order.cancellation_reason = reason
order.save()
return order
except Order.DoesNotExist:
raise ValueError("Заказ не найден или не может быть отменен")
@staticmethod
def get_order_statistics(days=30):
"""Получение статистики заказов"""
from django.db.models import Count, Sum, Avg
from django.utils import timezone
from django.db.models.functions import TruncDay
return Order.objects.filter(
created_at__gte=timezone.now() - timezone.timedelta(days=days)
).annotate(
day=TruncDay('created_at')
).values('day').annotate(
order_count=Count('id'),
total_revenue=Sum('total'),
avg_order_value=Avg('total')
).order_by('day')
class ProductService:
"""Сервис для работы с продуктами"""
@staticmethod
def get_popular_products(limit=10):
"""Получение популярных продуктов"""
return Product.objects.filter(
is_active=True
).annotate(
order_count=Count('orderitem')
).order_by('-order_count')[:limit]
@staticmethod
def get_similar_products(product, limit=5):
"""Получение похожих продуктов"""
return Product.objects.filter(
category=product.category,
is_active=True
).exclude(id=product.id)[:limit]
@staticmethod
def apply_product_discount(product_id, discount_percent):
"""Применение скидки к продукту"""
if not 0 <= discount_percent <= 100:
raise ValueError("Скидка должна быть в диапазоне 0-100")
product = Product.objects.get(id=product_id)
# Создание или обновление скидки
Discount.objects.update_or_create(
product=product,
defaults={'percent': discount_percent}
)
return product
Представления
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from .models import Order, Product
from .services import OrderService, ProductService
@login_required
def create_order_view(request):
"""Создание заказа"""
if request.method == 'POST':
try:
items_data = [
{
'product_id': int(pid),
'quantity': int(qty)
}
for pid, qty in request.POST.getlist('product_id', [])
]
order = OrderService.create_order(
user=request.user,
items_data=items_data,
shipping_address=request.POST.get('shipping_address')
)
return redirect('order_detail', order.id)
except ValueError as e:
products = Product.objects.filter(
id__in=request.POST.getlist('product_id')
)
return render(request, 'order/create.html', {
'error': str(e),
'products': products,
'quantities': dict(request.POST.items())
})
return render(request, 'order/create.html')
@login_required
def cancel_order_view(request, order_id):
"""Отмена заказа"""
if request.method == 'POST':
try:
OrderService.cancel_order(
order_id,
reason=request.POST.get('reason')
)
return redirect('my_orders')
except ValueError as e:
return render(request, 'order/cancel.html', {
'error': str(e),
'order': get_object_or_404(Order, id=order_id)
})
return render(request, 'order/cancel.html')
def product_detail_view(request, product_id):
"""Детальная страница продукта"""
product = get_object_or_404(Product, id=product_id, is_active=True)
similar_products = ProductService.get_similar_products(product)
return render(request, 'product/detail.html', {
'product': product,
'similar_products': similar_products
})
Пример 2: Блог с использованием QuerySet методов
Рассмотрим пример блога, где сложные запросы инкапсулированы в методы QuerySet.
Модели
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('DRAFT', 'Черновик'),
('PUBLISHED', 'Опубликовано'),
('ARCHIVED', 'В архиве'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='DRAFT')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, blank=True)
def __str__(self):
return self.title
def save(self, *args, **kwargs):
"""Переопределение save для автоматической публикации"""
if self.status == 'PUBLISHED' and not self.published_at:
self.published_at = timezone.now()
super().save(*args, **kwargs)
class Meta:
ordering = ['-created_at']
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
is_approved = models.BooleanField(default=True)
def __str__(self):
return f"Комментарий от {self.author.username} к {self.post.title}"
Менеджеры и методы QuerySet
# managers.py
from django.db import models
from django.db.models import Q, Count, Avg
from django.utils import timezone
class PublishedPostManager(models.Manager):
"""Менеджер для опубликованных постов"""
def get_queryset(self):
return super().get_queryset().filter(status='PUBLISHED')
class PostManager(models.Manager):
"""Основной менеджер постов"""
def get_popular_posts(self, days=30, limit=10):
"""Получение популярных постов за период"""
return self.filter(
status='PUBLISHED',
published_at__gte=timezone.now() - timezone.timedelta(days=days)
).annotate(
comment_count=Count('comments', filter=Q(comments__is_approved=True))
).order_by('-comment_count')[:limit]
def get_posts_by_category(self, category_slug):
"""Получение постов по категории"""
return self.filter(
status='PUBLISHED',
category__slug=category_slug
).select_related('category', 'author').prefetch_related('tags')
def get_related_posts(self, post, limit=3):
"""Получение связанных постов"""
return self.filter(
status='PUBLISHED',
category=post.category,
id__ne=post.id
).exclude(id=post.id)[:limit]
def get_posts_with_statistics(self):
"""Получение постов со статистикой"""
return self.annotate(
comment_count=Count('comments', filter=Q(comments__is_approved=True)),
avg_rating=Avg('postcomment__rating')
).filter(
status='PUBLISHED',
comment_count__gt=0
).order_by('-comment_count')
class Post(models.Model):
# Поля модели (как выше)
# ...
objects = PostManager()
published = PublishedPostManager()
@classmethod
def get_author_posts(cls, author, year=None, month=None):
"""Получение постов автора с фильтрацией по дате"""
queryset = cls.objects.filter(
status='PUBLISHED',
author=author
).select_related('category')
if year:
queryset = queryset.filter(
published_at__year=year
)
if month:
queryset = queryset.filter(
published_at__month=month
)
return queryset
@classmethod
def search_posts(cls, query):
"""Поиск постов"""
return cls.objects.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(tags__name__icontains=query),
status='PUBLISHED'
).distinct()
def get_comment_count(self):
"""Количество утвержденных комментариев"""
return self.comments.filter(is_approved=True).count()
def is_recently_published(self, days=7):
"""Является ли пост недавно опубликованным"""
return self.published_at and (
timezone.now() - self.published_at
).days <= days
Использование в представлениях
# views.py
from django.shortcuts import render, get_object_or_404
from .models import Post, Category
from .managers import Post
def blog_index_view(request):
"""Главная страница блога"""
popular_posts = Post.objects.get_popular_posts(limit=5)
recent_posts = Post.objects.filter(
status='PUBLISHED'
).order_by('-published_at')[:10]
return render(request, 'blog/index.html', {
'popular_posts': popular_posts,
'recent_posts': recent_posts
})
def category_posts_view(request, category_slug):
"""Посты категории"""
category = get_object_or_404(Category, slug=category_slug)
posts = Post.objects.get_posts_by_category(category_slug)
return render(request, 'blog/category.html', {
'category': category,
'posts': posts
})
def post_detail_view(request, slug):
"""Детальная страница поста"""
post = get_object_or_404(
Post,
slug=slug,
status='PUBLISHED'
)
# Получение связанных постов
related_posts = Post.objects.get_related_posts(post)
# Увеличение счетчика просмотров (опущено для краткости)
return render(request, 'blog/detail.html', {
'post': post,
'related_posts': related_posts
})
def search_view(request):
"""Поиск постов"""
query = request.GET.get('q', '')
if query:
posts = Post.objects.search_posts(query)
else:
posts = Post.objects.none()
return render(request, 'blog/search.html', {
'query': query,
'posts': posts
})
Лучшие практики
-
Следуйте принципу единственной ответственности — каждый класс или функция должна иметь одну четкую цель.
-
Используйте абстракции — определяйте интерфейсы для репозиториев и сервисов, чтобы можно было легко заменять реализации.
-
Обрабатывайте транзакции правильно — используйте
@transaction.atomicдля сложных операций, чтобы обеспечить целостность данных. -
Оптимизируйте запросы — используйте
select_related,prefetch_relatedи аннотации для уменьшения количества запросов к базе данных. -
Обрабатывайте ошибки — предоставляйте понятные сообщения об ошибках и логируйте исключения.
-
Тестируйте бизнес-логику — пишите юнит-тесты для сервисов и use cases, изолированно от инфраструктуры.
-
Документируйте код — пишите docstrings для сложных методов и классов, объясняя их предназначение и использование.
-
Используйте type hints — это улучшает читаемость кода и позволяет использовать статическую проверку типов.
-
Соблюдайте принцип DRY (Don’t Repeat Yourself) — выносите повторяющийся код в отдельные функции или классы.
-
Разделяйте данные и логику — модели должны отвечать только за хранение данных, а бизнес-логика должна находиться в сервисах или use cases.
Как отмечает proofit404 на Django Forum, ключ к хорошей архитектуре Django — это найти баланс между простотой и сложностью, не переусложняя приложение, но и не создавая “кучу говнокода” в моделях или представлениях.
Источники
-
Stack Overflow — Разделение бизнес-логики и доступа к данным в Django: https://stackoverflow.com/questions/12578908/separation-of-business-logic-and-data-access-in-django
-
GeeksforGeeks — Разделение бизнес-логики и доступа к данным в Python/Django: https://www.geeksforgeeks.org/python/separation-of-business-logic-and-data-access-in-django/
-
James Bennett Blog — Нет сервиса: лучшие практики Django: https://www.b-list.org/weblog/2020/mar/16/no-service/
-
PyPy Django Blog — Разделение бизнес-логики и представления в Django: https://pypy-django.github.io/blog/2024/05/15/separating-business-logic-and-presentation-in-django/
-
Django Forum — Где разместить бизнес-логику в Django: https://forum.djangoproject.com/t/where-to-put-business-logic-in-django/282
Заключение
Правильное разделение бизнес-логики и доступа к данным в Django является ключевым фактором создания масштабируемых, поддерживаемых и тестируемых приложений. В этой статье мы рассмотрели несколько архитектурных подходов, каждый из которых имеет свои преимущества и области применения.
Основные подходы включают:
-
Паттерн “толстые модели” — подходит для простых приложений и логики, связанной с одним объектом.
-
Сервисный слой — идеален для сложных бизнес-процессов, особенно тех, что охватывают несколько моделей.
-
Методы QuerySet и кастомные менеджеры — отличное решение для часто используемых шаблонов запросов.
-
Разделение команд и запросов (CQRS) — полезно для приложений с высокой нагрузкой на чтение.
-
Чистая и шестигранная архитектура — подход для сложных корпоративных приложений, требующих гибкости и тестируемости.
Выбор конкретного подхода зависит от размера проекта, сложности бизнес-логики и требований к производительности. Важно не переусложнять архитектуру для простых проектов, но и не создавать “кучу говнокода” в моделях или представлениях.
Главное правило — соблюдайте принцип единственной ответственности и стремитесь к четкому разделению слоев. Это позволит создавать гибкие приложения, которые легко развиваются и поддерживаются в долгосрочной перспективе.
В Django важно различать модель данных (data model) и доменную модель (domain model). Доменная модель содержит бизнес-логику и сущности, видимые пользователю, а модель данных — только хранение в БД.
Рекомендуемые подходы:
- Сервисный слой (
services.py) для централизации команд. - Django Forms для валидации и выполнения команд.
- Кастомные QuerySet и менеджеры для запросов.
Принципы чистоты кода:
- Методы модели управляют только состоянием БД.
- Свойства не ссылаются на внешнюю инфраструктуру.
- Представления не манипулируют моделями напрямую.
Для чистой архитектуры в Django разделяйте слои: домен, приложение, инфраструктуру. Сервисный слой (services.py) размещайте между моделями и views для бизнес-логики.
Другие практики:
- Кастомные менеджеры и QuerySet методы для сложных запросов.
- Формы Django для валидации бизнес-правил.
- Сигналы для событийной логики.
- Модели запросов для отчетов.
Поддерживаются чистая архитектура и шестигранная архитектура с адаптерами.
Django ORM уже предоставляет нужные операции, поэтому сервисный слой не обязателен. Размещайте бизнес-логику в моделях, менеджерах и подклассах QuerySet.
- Альтернативные конструкторы — в методы менеджера.
- Сложные запросы — в QuerySet методы.
- Логика одного экземпляра — в модель.
- Views остаются тонкими, вызывая публичные методы моделей.
Для сильной разделенности используйте Data-Mapper ORM вроде SQLAlchemy вместо обертки Django ORM.
Используйте классовые представления (CBV) для выноса логики в модели и сервисы. Принцип ‘тонкие views, толстые модели’:
- Бизнес-логика объекта — в методы модели.
- Views — только запросы и контекст.
- Сервис-слой для операций над несколькими моделями (статические методы в
services.py). - В шаблонах — кастомные теги и фильтры (
format_price), без логики.
Сервисы переиспользуемы в views, задачах и тестах.
Подходы к бизнес-логике в Django по мнению сообщества:
- Модели/менеджеры — для логики одного объекта (‘толстые модели’).
- Views — не рекомендуется, становятся большими.
- Сервисный слой (
services.py) — для сложных операций над моделями, API, задачами.
Рекомендации:
- Один объект — метод модели.
- Несколько объектов — менеджер/QuerySet.
- Кросс-модельные операции — сервисы.
Сервисный модуль — интерфейс приложения для переиспользования.