От переводчика
Как всегда вольный перевод интересной статьи о конкретном подходе к организации кода в django-приложениях. Будет полезна:
Большого количества кода не будет, статья по большей части дискуссионная. Энжой)
- Тем, кто еще не задумывался о таких вопросах
- Тем, кто уже имеет собственные взгляды на организацию логики, но не против оценить альтернативные варианты
- Тем, кто уже использует обсуждаемый подход, для подтверждения своих мыслей
- Тем, кто уже не использует обсуждаемый подход и имеет аргументы против
Большого количества кода не будет, статья по большей части дискуссионная. Энжой)
Толстые модели.
Интро
Большинство django-туториалов и примеров по этому фреймворку подают плохой пример новичкам в плане организации кода в их проектах. В случае крупного/долгоиграющего приложения, структура кода, подчерпнутая из подобных примеров, может стать причиной нетривиальных проблем и сложных ситуаций в процессе разработки. В данной статье я опишу альтернативный, достаточно редко встречающийся подход к организации кода, который, надеюсь, покажется вам интересным.
MVC в django = MTV + встроенное C
Пытались когда-нибудь объяснить как устроено MTV в django, скажем, RoR-девелоперу? Кто-то может подумать, что шаблоны – это представления, а представления – это контроллеры. Не совсем так. Контроллер – это встроенный в django URL-маршрутизатор, который обеспечивает логику запрос-ответ. Представления нужны для представления нужных данных в нужных шаблонах. Шаблоны и представления совокупно составляют «презентационный» слой фреймворка.
В подобном MTV много плюсов – с его помощью можно легко и быстро создавать типовые и не только приложения. Тем не менее, остаётся неясным где должна храниться логика по обработке и правке данных, куда абстрагировать код в каких случаях. Давайте оценим несколько разных подходов и посмотрим на результаты их применения.
Логика в представлениях
Засунуть всю или большую часть логики во вьюхи. Подход, наиболее часто встречающийся в различных туториалах и у новичков. Выглядит как-то так:
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
quote.accepted = True
quote.commission_paid = False
# назначаем комиссию
provider_credit_card = CreditCard.objects.get(user=quote.provider)
braintree_result = braintree.Transaction.sale({
'customer_id': provider_credit_card.token,
'amount': quote.commission_amount,
})
if braintree_result.is_success:
quote.commission_paid = True
transaction = Transaction(card=provider_credit_card,
trans_id = result.transaction.id)
transaction.save()
quote.transaction = transaction
elif result.transaction:
# обрабатываем ошибку, позже таск будет передан в celery
logger.error(result.message)
else:
# обрабатываем ошибку, позже таск будет передан в celery
logger.error('; '.join(result.errors.deep_errors))
quote.save()
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
Предельно просто, поэтому на первый взгляд привлекательно – весь код в одном месте, не нужно напрягать мозг и что-то там абстрагировать. Но это только на первый взгляд. Подобный подход плохо масштабируется и быстро приводит к потере читаемости. Я имел счастье лицезреть представления на полтысячи строк кода, от которых даже у матерых девелоперов сводило скулы и сжимались кулачки. Толстые вьюхи ведут к дублированию и усложнению кода, их тяжело тестировать и дебажить и, как следствие, – легко сломать.
Логика в формах
Формы в django объектно-ориентированы, в них происходит валидация и очистка данных, в силу чего их также можно рассматривать, как место размещения логики.
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
# инкапсулируем логику в форме
form.accept_quote()
success = form.charge_commission()
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
Уже лучше. Проблема в том, что теперь форма для приёма оплаты также занимается обработкой комиссий по кредитным картам. Некомильфо. Что если мы захотим использовать данную функцию в каком-то другом месте? Мы, разумеется, умны и могли бы закодить необходимые примеси, но опять-таки, что если данная логика понадобится нам в консоли, в celery или другом внешнем приложении? Решение инстанцировать форму для работы с моделью не выглядит правильным.
Код в представлениях на основе классов
Подход очень похож на предыдущий – те же преимущества, те же недостатки. У нас нет доступа к логике из консоли и из внешних приложений. Более того, усложняется схема наследования вьюх в проекте.
utils.py
Еще один простой и заманчивый подход – абстрагировать из представлений весь побочный код и вынести его в виде utility-функций в отдельный файл. Казалось бы, быстрое решение всех проблем (которое многие в итоге и выбирают), но давайте немного поразмыслим.
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
# инкапсулируем логику в utility-функции
accept_quote_and_charge(quote)
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
Первая проблема в том, что расположение таких функций может быть неочевидно, код относящийся к определённым моделям может быть размазан по нескольким пакетам. Вторая проблема в том, что когда ваш проект вырастет и над ним начнут работать новые / другие люди, будет неясно какие функции вообще существуют. Что, в свою очередь, увеличивает время разработки, раздражает девелоперов и может стать причиной дублирования кода. Определённые вещи можно отловить на код-ревью, но это уже решение постфактум.
Решение: толстые модели и жирные менеджеры
Модели и их менеджеры являются идеальным местом для инкапсуляции кода, предназначенного для обработки и обновления данных, особенно, когда такой код логически или функционально завязан на возможности ORM. По сути, мы расширяем API модели собственными методами.
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
# инкапсулируем логику в методе модели
quote.accept()
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
На мой вкус, подобное решение является самым правильным. Код по обработке кредитных карт изящно инкапсулирован, логика находится в релевантном для неё месте, нужную функциональность легко найти и (пере)использовать.
Резюме: общий алгоритм
Куда писать код
- Код в методе модели
- Код в методе менеджера
- Код в методе формы
- Код в методе CBV
Если ни один из вариантов не подошел, возможно стоит рассмотреть абстрагирование в отдельную utility-функцию.
TL;DR
Логика в моделях улучшает django-приложения
Бонус
В комментариях к оригинальному топику проскользнули ссылки на два интересных приложения, близких теме статьи.
github.com/kmmbvnr/django-fsm – поддержка конечного автомата для django-моделей (из описания). Устанавливаете на модель поле FSMField и отслеживаете изменение заранее предопределенных состояний с помощью декоратора в духе receiver.
github.com/adamhaney/django-ondelta – примесь для django-моделей, позволяющая обрабатывать изменения в полях модели. Предоставляет API в стиле собственных clean_*-методов модели. Делает именно то, что указано в описании.
Там же был предложен еще один подход – абстрагировать весь код, относящийся к бизнес-логике в отдельный модуль. Например, в приложении prices выделяем весь код, ответственный за обработку цен, в модуль processing. Сходно с подходом utils.py, отличается тем, что абстрагируем бизнес-логику, а не всё подряд.
В собственных проектах я в целом использую подход автора статьи, придерживаясь такой логики:
- Код в методе модели – если код относится к конкретному инстансу модели
- Код в методе менеджера – если код затрагивает всю соответствующую таблицу
- Код в методе формы – если код валидирует и/или предобрабатывает данные из запроса
- Код в методе CBV – то, что относится к request и по остаточному принципу
- В utils.py – код, не относящийся напрямую к проекту
Обсудим?