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

http://redbeacon.github.io/2014/01/28/Fat-Models-a-Django-Code-Organization-Strategy/
  • Перевод
От переводчика
Как всегда вольный перевод интересной статьи о конкретном подходе к организации кода в django-приложениях. Будет полезна:
  • Тем, кто еще не задумывался о таких вопросах
  • Тем, кто уже имеет собственные взгляды на организацию логики, но не против оценить альтернативные варианты
  • Тем, кто уже использует обсуждаемый подход, для подтверждения своих мыслей
  • Тем, кто уже не использует обсуждаемый подход и имеет аргументы против

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


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 – код, не относящийся напрямую к проекту

Обсудим?
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 21
  • +3
    Код в методе модели – если код относится к конкретному инстансу модели
    Код в методе менеджера – если код затрагивает всю соответствующую таблицу

    Куда помещаете код, который работает с несколькими таблицами одновременно, да так, что даже одну основную из них выделить затруднительно?
    • 0
      Зависит от назначения кода. Если основную выделить сложно, скорее всего во вьюху, возможно в отдельный файл.
      • 0
        В отдельный файл, а там даже может быть в класс?
      • 0
        Фейспалм. Следите за мыслью автора:

        Логика во вьюхе — плохо.
        Логика в форме — плохо.
        Логика в utils — плохо.
        ВНЕЗАПНО логика в моделях — хорошо.

        Логика должна быть в logic.py — потому что это логика. А методы на 3 экрана в моделях — говно.

        • +1
          Автор говорит в основном о логике, касающейся работы с данными из моделей. Почему не разместить её в методах модели или менеджера?

          > Логика должна быть в logic.py — потому что это логика. А методы на 3 экрана в моделях — говно.

          А если конструктивно? Вы предлагаете всю логику писать в отдельном файле?
          • –5
            Да.
            • –1
              Слишком категорично. Возможно вы правы в случае крупных проектов, но логика бывает разная и иногда в методах модели/менеджера ей самое место.
            • +2
              Вообще про логику в моделях еще в Django Two Scoops (самая крутая книга по джанге на мой взгляд, ссыль) написали, но этож ад!

              Приведу пример. Мы разрабатывали CRM, которое обеспечивало все рабочие процессы компании. В кратце, там был прием и обработка заявок со сложной финансовой калькуляцией, а также статистика. Было у нас все «хрестоматийно»: толстые модели с логикой, причем функции логики вызывались прямо из теплейта. Сначала все было ок. Но по мере нарастания функционала начались Содом и Гоморра =)

              Во-первых, нельзя передавать параметры в функции в шаблоне (тэги не в счет, они для другого). Во-вторых, постоянный пересчет и вызов этих функций. В-третьих (что оказалось самым страшным), сложная модифицируемость кода — когда нужно было сделать серьезнейшее изменение в калькуляции и формате хранимых данных, дров было наломано неописуемо много!

              В итоге, я соглашаюсь с тем, что всю сложную логику нужно выносить в отдельные службы. Ну а в модель выносить то, что касается непосредственно этой модели, вроде get_то, set_се.

              • 0
                Спасибо за ссылку. Для интересующихся: там же есть издание по 1.6 (но только в бумаге, в 2х дороже и на 1.3х объемнее).
            • +5
              Вероятно речь идёт о бизнес-логике, которой самое место в модели.
            • +3
              Зря КДПВ убрали, довольно забавная игра слов была. :)
              • +1
                Видимо, кто-то посчитал её недостаточно толерантной)
              • +5
                Частая ошибка при переносе логики в модель (или utility-модуль) — передача в эти методы request-а (а порой в ответ еще отрендеренный шаблон приходит).
                В таком подходе никакой инкапсуляцией и не пахнет — получается размазанная по куче файлов view с невозможностью вызывать методы бизнес-логики из сторонних приложений или тестов.
                • +3
                  Автор об этом упоминает: «Если ваша логика завязана на объект request, то ей, вероятно, самое место в представлении.». Т.е. можно закодить как примесь для CBV, но тут уже по ситуации.
                • +2
                  Вопрос организации больших объемов кода хорошо раскрыт в книгах по Domain Driven Design. То, что предлагает автор оригинальной статьи — архитектурное самоубийство.
                  • +4
                    Следует помнить, что вообще в джанге как таковой сервисный слой из коробки не предусмотрен, не считая middleware-ей, но у них немного иное предназначение, afaik.

                    Поэтому мне не кажется, что закинуть часть логики в модель это «архитектурное самоубийство», не говоря уже о том, что это никак не противоречит DDD.
                    Главное не забыть, что вся валидация-рендеринг и, возможно, поднятие модельных объектов должно остаться на уровне View, а модель должна работать на уровне именно модели — т.е. взаимодействовать с другими бизнес-объектами и ничего не знать о View.
                    • +5
                      > не говоря уже о том, что это никак не противоречит DDD.

                      Вот с этим утверждением частично не соглашусь.

                      Модель в Джанге — это Entity в DDD, которая при наличии ORM служит также и компонентом Repository. Если у нас есть логика, которая явно отвечает за работу с данными в репозитории, мы смело присоединяем её к модели. Примером такой логики может служить шардинг данных между разными кластерами. Стандартная практика переопределять «Model.save()» — пример такой логики.

                      Но вот в статье автор приводит пример с «quote.accept()». Эта практика противоречит принципам DDD, так как в определении функции мы видим несколько этапов:

                      — валидация запроса
                      — вызов стороннего компонента (braintree.Transaction.sale)
                      — обработка результата операции (Transaction.save() + quote.save()).

                      В терминах DDD это будут:
                      — Validation (пользовательский запрос содержит корректные данные на входе)
                      — Policy Check (аккаунт содержит информацию о кредитной карте)
                      — Service Call (braintree.Transaction.sale())
                      — Service Call (Transaction.save())
                      — Policy Check (транзакция была успешно зарегистрирована в системе)
                      — Repository Call (quote.save())

                      Если мы помещаем всю эту логику в модель Quote, то мы сразу нарушем Single Responsibility Principle, так как Quote у нас оперирует терминами Transaction и Credit Card, тогда как роль любой сущености — самоидентификация.

                      Поэтому я бы поместил эту логику в AcquiringService, который оперирует API из:

                      — AccountService — получение информации о кредитке и пользователе
                      — TransactionService — атомарное логгирование транзакций)
                      — AquiringPolicy — проверка согласованности между сущностями Account, Quote, Transaction.

                      В итоге получается, что сервисы общаются между собой, правила общения определяют Policies описанные в бизнес-терминах, сущности предоставляют самоидентификацию бизнес-моделей, но ничего не знают о других сущностях, а Django Views служат в качестве терминала доступа к бизнес-логике приложения через HTTP.
                  • +6
                    Советую всем, кто задается тем же вопросом, что и автор, но не считает решения автора удовлетворительным, ознакомится с ответами на мой похожий вопрос, поставленный на stackovertflow полтора года назад:

                    stackoverflow.com/questions/12578908/separation-of-business-logic-and-data-access-in-django
                    • +3
                      Отмечусь тут как автор django-fsm

                      django-fsm — хорошо служит для описания логики самой модели, и связи между полями модели, а не для того чтобы засовывать в модель все что угодно.

                      Если при подтверждении заказа вам надо поставить accepted_time, самое естественное место для этого — это метод модели. Если при оплате, мы цепляем транзакцию к заказу, экземпляр транзакции можно передать в метод, меняющий статус заказа. Отправленный заказ не может просто так встать на отправку опять, потому что мы по ошибке забыли где-то спрятать ссылку или злоумышленник сделал прямой POST запрос, а мы забыли проверить условие в одном из view — это тоже хорошо можно проконтролировать на уровне модели.

                      Пытаться сделать что-то большее, это наступать на грабли.

                      Я уже раза три реджектил pull request'ы от разных людей пытающихся добавить передачу параметров в pre_translation условия. Потому что единственный внятный usе-case озвученный авторами была проверка прав пользователя на совершения данной операции.

                      Аналогично, про попытки добавления логирования изменений. Уровень модели это не самое удачное место для этого. Те ухищрения с post_save сигналами которые пришлось применить авторам, уже явно на это указывают.
                      • 0
                        Контроллер – это встроенный в django URL-маршрутизатор, который обеспечивает логику запрос-ответ.

                        Несколько странное утверждение, оно кажется мне странным и скорее всего ошибочным.

                        A controller can send commands to the model to update the model's state (e.g., editing a document). It can also send commands to its associated view to change the view's presentation of the model (e.g., by scrolling through a document).

                        http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
                        Django URL маршрутизатор вообще ничего не знает о моделях, тем более о их состоянии.

                        Ссылка по теме: Django MVC или MTV.
                        • +1
                          Года два назад меня посетила мысль, что в Django явно не хватает ещё одного слоя — «бизнес-логики». Сейчас понимаю, что вопрос гораздо глубже… Те кто пишет про всякие SOLID, DDD и пр. явно правее тех, кто выбирает между толстой моделью, толстым менеджером, толстым view, толстой формой или отдельным logic.py :)

                          Забавно, что к пониманию многих вещей меня подтолкнуло погружение в JavaScript, и, конкретно вот эта статья на Хбре. Знаю, в среде питонистов часто бытует религиозная ненависть к яваскрипту — абстрагируйтесь, почитайте, статья больше о понимании принципов чем о языке.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.