Веб-фулстак
0,0
рейтинг
26 февраля 2014 в 10:53

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

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

Обсудим?
Перевод: John Schulte
Николай @rzhannoy
карма
23,2
рейтинг 0,0
Веб-фулстак
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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, и, конкретно вот эта статья на Хбре. Знаю, в среде питонистов часто бытует религиозная ненависть к яваскрипту — абстрагируйтесь, почитайте, статья больше о понимании принципов чем о языке.

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