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

в 6:53, , рубрики: django, python, конкретно вы прочитали теги, организация кода, Совершенный код, толстые модели
От переводчика

Как всегда вольный перевод интересной статьи о конкретном подходе к организации кода в 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 – код, не относящийся напрямую к проекту

Обсудим?

Автор: rzhannoy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js