← Назад к вопросам
Где хранишь бизнес-логику в Django?
2.2 Middle🔥 181 комментариев
#Django#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Где хранишь бизнес-логику в Django?
Это один из самых важных вопросов архитектуры. Бизнес-логика НЕ должна быть в views — это частая ошибка новичков. Правильный подход следует принципам clean architecture и DDD (Domain-Driven Design).
Где НЕЛЬЗЯ хранить бизнес-логику
❌ Плохо: бизнес-логика в views
# views.py
from django.http import JsonResponse
from rest_framework.views import APIView
from .models import Order, Product, Payment
class CreateOrderView(APIView):
def post(self, request):
product_id = request.data.get('product_id')
quantity = request.data.get('quantity')
# ❌ Вся логика в view!
product = Product.objects.get(id=product_id)
if product.stock < quantity:
return JsonResponse({"error": "Not enough stock"}, status=400)
total_price = product.price * quantity
if product.discount > 0:
total_price = total_price * (1 - product.discount / 100)
if product.category == 'digital':
tax = total_price * 0.20
else:
tax = total_price * 0.18
final_price = total_price + tax
# Сложная логика платежа
payment = Payment.objects.create(
amount=final_price,
status='pending'
)
order = Order.objects.create(
product=product,
quantity=quantity,
total_price=final_price,
payment=payment
)
product.stock -= quantity
product.save()
return JsonResponse({"order_id": order.id})
Проблемы:
- Непонятная логика
- Не переиспользуется (не можем вызвать из CLI, celery и т.д.)
- Сложно тестировать
- Нарушает принцип SRP (Single Responsibility)
Правильный подход: Services (UseCase)
Структура проекта:
app/
├── models.py # Django модели (ORM)
├── views.py # HTTP handlers (тонкие)
├── urls.py
├── serializers.py # Pydantic/DRF валидация
├── services/ # Бизнес-логика (сервисы)
│ ├── __init__.py
│ ├── order_service.py
│ ├── payment_service.py
│ └── inventory_service.py
├── repositories/ # Доступ к БД
│ ├── __init__.py
│ └── order_repository.py
└── tests/ # Тесты
Пример: Service Layer
1. Models (ORM)
# models.py
from django.db import models
from decimal import Decimal
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField()
discount = models.DecimalField(max_digits=3, decimal_places=2, default=0)
category = models.CharField(max_length=50)
class Order(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
total_price = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
class Payment(models.Model):
order = models.OneToOneField(Order, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, default='pending')
2. Repository (доступ к БД)
# repositories/order_repository.py
from ..models import Order, Product, Payment
from decimal import Decimal
class OrderRepository:
"""Доступ к заказам в БД"""
@staticmethod
def get_product(product_id: int) -> Product:
return Product.objects.get(id=product_id)
@staticmethod
def create_order(product: Product, quantity: int, total_price: Decimal) -> Order:
return Order.objects.create(
product=product,
quantity=quantity,
total_price=total_price
)
@staticmethod
def reduce_stock(product: Product, quantity: int) -> None:
product.stock -= quantity
product.save()
3. Service (бизнес-логика)
# services/order_service.py
from decimal import Decimal
from typing import Dict, Any
from ..models import Product, Order
from ..repositories.order_repository import OrderRepository
class OrderService:
"""UseCase: создание заказа"""
def __init__(self, repository: OrderRepository):
self.repository = repository
def create_order(self, product_id: int, quantity: int) -> Dict[str, Any]:
# 1. Валидация
product = self.repository.get_product(product_id)
if product.stock < quantity:
raise ValueError(f"Insufficient stock: {product.stock}")
# 2. Расчет цены
total_price = self._calculate_price(product, quantity)
# 3. Создание заказа
order = self.repository.create_order(product, quantity, total_price)
# 4. Снижение запаса
self.repository.reduce_stock(product, quantity)
return {
"order_id": order.id,
"total_price": float(total_price),
"status": "created"
}
def _calculate_price(self, product: Product, quantity: int) -> Decimal:
"""Расчет финальной цены с налогом и скидкой"""
base_price = product.price * quantity
# Применяем скидку
discounted_price = base_price * (1 - product.discount / 100)
# Применяем налог
tax_rate = Decimal('0.20') if product.category == 'digital' else Decimal('0.18')
tax = discounted_price * tax_rate
return discounted_price + tax
4. View (HTTP handler — тонкий)
# views.py
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import CreateOrderSerializer
from .services.order_service import OrderService
from .repositories.order_repository import OrderRepository
class CreateOrderView(APIView):
def post(self, request):
try:
# 1. Валидация входных данных
serializer = CreateOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 2. Вызов бизнес-логики
service = OrderService(OrderRepository())
result = service.create_order(
product_id=serializer.validated_data['product_id'],
quantity=serializer.validated_data['quantity']
)
# 3. Возврат результата
return Response(result, status=status.HTTP_201_CREATED)
except ValueError as e:
return Response(
{"error": str(e)},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
return Response(
{"error": "Internal server error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
Тестирование
Тестируем сервис независимо от Django:
# tests/test_order_service.py
import pytest
from decimal import Decimal
from unittest.mock import Mock
from app.services.order_service import OrderService
from app.models import Product
class TestOrderService:
def test_create_order_success(self):
# Arrange
product = Product(
id=1,
name="Laptop",
price=Decimal('1000'),
stock=10,
discount=Decimal('10'),
category='hardware'
)
mock_repository = Mock()
mock_repository.get_product.return_value = product
mock_repository.create_order.return_value = Mock(id=1)
service = OrderService(mock_repository)
# Act
result = service.create_order(product_id=1, quantity=2)
# Assert
assert result['order_id'] == 1
assert result['status'] == 'created'
mock_repository.reduce_stock.assert_called_once_with(product, 2)
def test_create_order_insufficient_stock(self):
# Arrange
product = Product(
id=1,
name="Laptop",
price=Decimal('1000'),
stock=1,
discount=Decimal('0'),
category='hardware'
)
mock_repository = Mock()
mock_repository.get_product.return_value = product
service = OrderService(mock_repository)
# Act & Assert
with pytest.raises(ValueError):
service.create_order(product_id=1, quantity=10)
Альтернатива: Model Methods (для простой логики)
Для простой логики допустимо добавить методы в модель:
# models.py
class Order(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
total_price = models.DecimalField(max_digits=10, decimal_places=2)
def calculate_total_with_tax(self) -> Decimal:
"""Метод модели для расчета с налогом"""
tax_rate = Decimal('0.20') if self.product.category == 'digital' else Decimal('0.18')
return self.total_price * (1 + tax_rate)
Но это только для простых операций, не для сложной бизнес-логики!
Layered Architecture (рекомендуется)
Presentation (Views) → Application (Services) → Domain (Models) → Infrastructure (ORM)
↓ ↓ ↓ ↓
HTTP requests Use cases, validation Business rules Database operations
зависимости идут только ВНИЗ ↓
Views не должны знать про Services
Services не должны знать про Views
Заключение
Правильное место для бизнес-логики:
- Services (UseCase) — основное место (70%)
- Model methods — простая логика (20%)
- Views — только HTTP обработка (10%)
Этот подход обеспечивает:
- Переиспользуемость кода
- Тестируемость
- Понятную архитектуру
- Соответствие SOLID принципам