← Назад к вопросам

Где хранишь бизнес-логику в 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

Заключение

Правильное место для бизнес-логики:

  1. Services (UseCase) — основное место (70%)
  2. Model methods — простая логика (20%)
  3. Views — только HTTP обработка (10%)

Этот подход обеспечивает:

  • Переиспользуемость кода
  • Тестируемость
  • Понятную архитектуру
  • Соответствие SOLID принципам