Привет, Хабр!
В Джанге существует множество глубоко укоренившиеся привычек, которые кажутся правильными на первый взгляд, но в долгосрочной перспективе приводят к серьезным проблемам в производительности, масштабируемости и безопасности проекта. Эти решения могут казаться удобными костылями или временными фиксами, но на самом деле они создают технический долг, который со временем будет только расти, усложняя все с каждым разом.
Умение избегать этих привычек – это основополагающие элементы компетентности, гарантирующие, что проекты будут не только удобными для пользователя, но и устойчивыми к проблемам.
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-разработчика.