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

Толстые модели.
Интро
Большинство 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/

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