Об организации кода в django-приложениях или толстые модели – это прекрасно

от автора

От переводчика

Как всегда вольный перевод интересной статьи о конкретном подходе к организации кода в django-приложениях. Будет полезна:

  • Тем, кто еще не задумывался о таких вопросах
  • Тем, кто уже имеет собственные взгляды на организацию логики, но не против оценить альтернативные варианты
  • Тем, кто уже использует обсуждаемый подход, для подтверждения своих мыслей
  • Тем, кто уже не использует обсуждаемый подход и имеет аргументы против

Большого количества кода не будет, статья по большей части дискуссионная. Энжой)

image

Не то, простите.

image
Толстые модели.

Интро

Большинство 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) 

На мой вкус, подобное решение является самым правильным. Код по обработке кредитных карт изящно инкапсулирован, логика находится в релевантном для неё месте, нужную функциональность легко найти и (пере)использовать.

Резюме: общий алгоритм

Куда писать код бле@ть? Если ваша логика завязана на объект request, то ей, вероятно, самое место в представлении. В противном случае, рассмотрите следующий порядок вариантов:

  • Код в методе модели
  • Код в методе менеджера
  • Код в методе формы
  • Код в методе 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 – код, не относящийся напрямую к проекту

Обсудим?

ссылка на оригинал статьи http://habrahabr.ru/post/213875/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *