Некоторые антипаттерны проектирования в Django

2d869b34c257aabb99b3b3cca19abedc.png

Привет, Хабр!

В Джанге существует множество глубоко укоренившиеся привычек, которые кажутся правильными на первый взгляд, но в долгосрочной перспективе приводят к серьезным проблемам в производительности, масштабируемости и безопасности проекта. Эти решения могут казаться удобными костылями или временными фиксами, но на самом деле они создают технический долг, который со временем будет только расти, усложняя все с каждым разом.

Умение избегать этих привычек — это основополагающие элементы компетентности, гарантирующие, что проекты будут не только удобными для пользователя, но и устойчивыми к проблемам.

Fat Models

В Django модели позволяет манипулировать данными, используя методы и свойства модели. Почему бы не использовать эту возможность? , — может спросить начинающий разраб, увидев преимущества такого подхода в краткосрочной перспективе.

Однако, дьявол кроется в деталях. Со временем такие модели начинают превращаться в монолитные блоки кода, которые трудно поддерживать, тестировать и масштабировать.

Чем больше логики содержится в модели, тем труднее становится понимать и модифицировать ее без риска нарушить что-то в другом месте приложения.

Представим, что есть веб-приложение на Django для управления проектами. В нем есть модель Project, которая, следуя подходу толстых моделей, содержит большую часть бизнес-логики приложения:

from django.db import models

class Project(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField()
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    status = models.CharField(max_length=10, choices=[('active', 'Active'), ('closed', 'Closed')])

    def is_active(self):
        return self.status == 'active'

    def close_project(self):
        self.status = 'closed'
        self.save()

    def extend_project(self, new_end_date):
        if self.is_active() and new_end_date > self.end_date:
            self.end_date = new_end_date
            self.save()
        else:
            raise ValueError("Project cannot be extended")

Project модель не только хранит данные о проектах, но также имеет методы для управления статусом проекта is_active, close_project, extend_project. Это оч. распространенная проблема.

Альтернативы

Сервисный слой представляет собой отдельный слой абстракции, который содержит бизнес-логику приложения, отделяя её от представления данных и пользовательского интерфейса.

Допустим, есть приложение для управления задачами, и надо реализовать функционал завершения задачи:

# models.py
from django.db import models

class Task(models.Model):
    title = models.CharField(max_length=255)
    completed = models.BooleanField(default=False)

# services.py
def complete_task(task_id):
    from .models import Task
    task = Task.objects.get(id=task_id)
    task.completed = True
    task.save()

Логика завершения задачи вынесена из модели в отдельный сервисный слой сервис пай. т.е функция complete_task независимой от конкретной реализации модели

Паттерн репозиторий предлагает абстрагироваться от специфики хранения данных, предоставляя собой интерфейс для доступа к данным, который не зависит от конкретной реализации хранения:

# repository.py
class TaskRepository:
    def get(self, task_id):
        from .models import Task
        return Task.objects.get(id=task_id)

    def update(self, task, **kwargs):
        for key, value in kwargs.items():
            setattr(task, key, value)
        task.save()

# services.py
def complete_task(task_id):
    from .repository import TaskRepository
    repo = TaskRepository()
    task = repo.get(task_id)
    repo.update(task, completed=True)

TaskRepository служит абстракцией для доступа к данным модели Task. Сервис complete_task использует репозиторий для изменения статуса задачи, что делает код более гибким и удобным для тестирования.

Magic numbers и hardcoding

Magic numbers — это литералы, которые встречаются в коде без объяснения, что они означают или почему были выбраны (ну иногда бывают комменты). Например, if (user.age > 18) — здесь 18 является магическим числом. Почему 18? В большинстве контекстов это возраст совершеннолетия, но без контекста или комментария это просто число, которое может внезапно измениться в зависимости от требований бизнеса или законодательства.

Все магические числа следует выносить в константы, это упрощает изменения в будущем:

# bad
if user.age > 18:
    # делаем что-то

# good
LEGAL_AGE = 18
if user.age > LEGAL_AGE:
    # делаем что-то

Жесткое кодирование — это когда вы прямо в коде задаете конфигурационные данные, пути доступа, пароли, URL и так далее. Это может показаться удобным на первый взгляд, но только до первого крупного изменения или необходимости деплоя в новом окружении.

Более сложные настройки и данные, которые могут изменяться в зависимости от среды выполнения, следует выносить в конфигурационные файлы. Это может быть .env файл для переменных окружения, config.json, settings.py или любой другой формат:

{
    "database": {
        "host": "localhost",
        "port": 3306,
        "username": "user",
        "password": "pass"
    }
}

Spaghetti templates

Эта проблема возникает, когда шаблоны становятся слишком загруженными, сложными и запутанными, напоминая спагетти. Это происходит из-за избыточного использования логики в шаблонах, сложной вложенности, и чрезмерного количества шаблонных тегов и фильтров.

Это вызывает ряд проблем и первая и, пожалуй, самое очевидное — это ухудшение читаемости и поддерживаемости кода. Когда новый человек приходит в проект и видит шаблон, изобилующий сложной логикой и вложенностями, разобраться в нем становится не просто сложно, но и времязатратно.

Решение:

Custom template tags позволяют расширять стандартный набор возможностей шаблонизатора Django, добавляя собственную логику обработки данных или создавая пользовательские фильтры.

Сustom tag для вывода текущего времени:

from django import template
import datetime

register = template.Library()

@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

Inclusion tags подходят для создания компонентов, которые можно многократно использовать в различных частях приложения.

Inclusion tag для отображения пользовательского профиля:

from django import template
register = template.Library()

@register.inclusion_tag('my_app/user_profile.html')
def user_profile(user):
    return {'user': user}

Ну и самый на мой взгляд важный принцип, обеспечивающий чистоту и читаемость кода, заключается в строгом разделении логики приложения. Это достигается за счёт использования view для обработки данных и шаблонов для их отображения. Также иногда можно использовать django forms, они могут автоматизируя создание, валидацию и отображение полей формы.

Игнор возможностей ORM

Часто некоторые игнорят возможности ORM, а зря.

annotate позволяет добавлять к запросам доп. поля, которые могут быть вычислены с использованием агрегирующих функций, Count, Sum, Avg и др:

from django.db.models import Count
from myapp.models import Author

# подсчет количества книг у каждого автора
authors = Author.objects.annotate(books_count=Count('book')).order_by('-books_count')
for author in authors:
    print(f"{author.name} имеет {author.books_count} книг(и)")

aggregate используется для вычисления агрегированных значений по всему QuerySet:

from django.db.models import Avg, Max, Min, Sum
from myapp.models import Order

# получение общей суммы всех заказов
total_sales = Order.objects.aggregate(total=Sum('amount'))
print(f"Общая сумма продаж: {total_sales['total']}")

# вычисление средней, максимальной и минимальной суммы заказа
sales_stats = Order.objects.aggregate(Avg('amount'), Max('amount'), Min('amount'))
print(f"Статистика продаж: Средняя сумма - {sales_stats['amount__avg']}, Макс. - {sales_stats['amount__max']}, Мин. - {sales_stats['amount__min']}")

Объекты F позволяют ссылаться на значения полей модели непосредственно в запросе:

from django.db.models import F
from myapp.models import Product

# увеличение цены всех продуктов на 10%
Product.objects.update(price=F('price') * 1.1)

Можно создавать сложные запросы, включая использование Q объектов для формирования сложных условий фильтрации и Prefetch для оптимизации запросов к связанным объектам:

from django.db.models import Q, Prefetch
from myapp.models import Blog, Entry, Comment

# получение блогов, содержащих записи с количеством комментариев больше 5
blogs = Blog.objects.prefetch_related(
    Prefetch('entry_set', queryset=Entry.objects.filter(comments__gt=5))
)

# пример сложной фильтрации с использованием Q объектов
entries = Entry.objects.filter(
    Q(comments__gt=5) | Q(title__icontains='django')
)

А какие антипаттерны в Django самые серьезные на ваш взгляд?

Данная статья подготовлена в рамках запуска специализации Python Developer. На странице специализации вы можете зарегистрироваться на бесплатные уроки про декораторы и SQL для Python-разработчика.

© Habrahabr.ru