Разделение бизнес-логики и доступа к данным в, Джанго


Я пишу проект в Django и вижу, что 80% кода находится в файле models.py. Этот код сбивает с толку, и через некоторое время я перестаю понимать, что происходит на самом деле.

вот что меня беспокоит:

  1. Я нахожу это уродливым, что мой уровень модели (который должен был быть отвечает только за работу с данными из базы данных) тоже отправки сообщений электронной почты, идя по API для других сервисов и т. д.
  2. кроме того, я считаю недопустимым поместите бизнес-логику в представление, потому что таким образом, становится трудно контролировать. Например, в моем приложение существует как минимум три способа создания новых экземпляры User, но технически он должен создать их равномерно.
  3. Я не всегда замечаю, когда методы и свойства моих моделей становятся недетерминированными и когда они развиваются побочные явления.

вот простой пример. Во-первых,User модель нравится это:

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

со временем это превратилось в такое:

class User(db.Models):

    def get_present_name(self): 
        # property became non-deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

    def activate(self):
        # method now has a side effect (send message to user)
        self.status = 'activated'
        self.save()
        send_mail('Your account is activated!', '…', [self.email])

Я хочу разделить сущности в моем коде:

  1. сущности моей базы данных, уровень базы данных: что содержит мое приложение?
  2. сущности моего приложения, уровень бизнес-логики: что может сделать мое приложение?

каковы хорошие практики для реализации такого подхода, который может быть применен в Django?

8 344

8 ответов:

похоже, вы спрашиваете о разнице между модель данных и модель предметной области – последний-это место, где вы можете найти бизнес-логику и сущности, воспринимаемые вашим конечным пользователем, первый-это место, где вы фактически храните свои данные.

кроме того, я интерпретировал 3-ю часть вашего вопроса как: как заметить неспособность сохранить эти модели отдельно.

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

о модели домена

первое, что вам нужно признать, это то, что ваша модель домена на самом деле не о данных; это о действия и вопросы например, "активировать этого пользователя", "деактивировать этого пользователя", " Какие пользователи в настоящее время активированы?", и "как зовут этого пользователя?". В классических терминах: это о запросы и команды.

мышление в командах

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

дано неактивный пользователь
, когда администратор активирует этого пользователя
затем пользователь становится активным
и подтверждение по электронной почте отправляется пользователю
и в системный журнал добавляется запись
(прием. так далее.)

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

такой сценарий также действительно поможет вам в настройке тестовой среды разработки.

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

Команды, Выразив

Django предоставляет два простых способа выражения команд; они оба являются допустимыми вариантами, и нет ничего необычного в смешивании двух подходов.

уровень сервиса

The сервис модуль уже описано @Hedde. Здесь вы определяете отдельный модуль, и каждая команда представляется в виде функции.

services.py

def activate_user(user_id):
    user = User.objects.get(pk=user_id)

    # set active flag
    user.active = True
    user.save()

    # mail user
    send_mail(...)

    # etc etc

используя формы

другой способ-использовать форму Django для каждой команды. Я предпочитаю этот подход, потому что он сочетает в себе несколько тесно связанных аспектов:

  • выполнение команды (что он делает?)
  • проверка из параметров команды (может ли это сделать?)
  • презентация команды (как я могу это сделать?)

forms.py

class ActivateUserForm(forms.Form):

    user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
    # the username select widget is not a standard Django widget, I just made it up

    def clean_user_id(self):
        user_id = self.cleaned_data['user_id']
        if User.objects.get(pk=user_id).active:
            raise ValidationError("This user cannot be activated")
        # you can also check authorizations etc. 
        return user_id

    def execute(self):
        """
        This is not a standard method in the forms API; it is intended to replace the 
        'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern. 
        """
        user_id = self.cleaned_data['user_id']

        user = User.objects.get(pk=user_id)

        # set active flag
        user.active = True
        user.save()

        # mail user
        send_mail(...)

        # etc etc

мышление в запросах

в вашем примере не было никаких запросов, поэтому я взял на себя смелость составить несколько полезных запросов. Я предпочитаю использовать термин "вопрос", но запросы-это классическая терминология. Интересные вопросы: "Как зовут этого пользователя?", "Может ли этот пользователь войти в систему?", "Покажите мне список деактивированных пользователей "и" каково географическое распределение деактивированных пользователей?"

прежде чем приступить к ответу на эти вопросы, вы всегда должны задать себе два вопроса: это представления запрос только для моих шаблонов, и / или бизнес-логики запрос привязан к выполнению моих команд, и / или отчетность запрос.

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

другой вопрос: "есть ли у меня полный контроль над ответами?"Например, при запросе имени пользователя (в этом контексте) у нас нет никакого контроля над результатом, потому что мы полагаемся на внешний API.

оформление Запросы

самый простой запрос в Django-это использование объекта Manager:

User.objects.filter(active=True)

конечно, это работает только, если данные представлены в модели данных. Это не всегда так. В этих случаях вы можете рассмотреть следующие варианты.

пользовательские теги и фильтры

первый вариант полезен для запросов, которые являются лишь представления: пользовательские теги и фильтры шаблона.

шаблон.HTML-код

<h1>Welcome, {{ user|friendly_name }}</h1>

template_tags.py

@register.filter
def friendly_name(user):
    return remote_api.get_cached_name(user.id)

методы запроса

если ваш запрос не просто презентационный, вы можете добавить запросы к вашему services.py (Если вы используете это), или ввестиqueries.py модуль:

queries.py

def inactive_users():
    return User.objects.filter(active=False)


def users_called_publysher():
    for user in User.objects.all():
        if remote_api.get_cached_name(user.id) == "publysher":
            yield user 

Прокси-моделей

прокси-модели очень полезны в контексте бизнес-логики и представления. Вы в основном определяете расширенное подмножество своей модели. Вы можете переопределить базовый набор запросов менеджера, переопределив Manager.get_queryset() метод.

models.py

class InactiveUserManager(models.Manager):
    def get_queryset(self):
        query_set = super(InactiveUserManager, self).get_queryset()
        return query_set.filter(active=False)

class InactiveUser(User):
    """
    >>> for user in InactiveUser.objects.all():
    …        assert user.active is False 
    """

    objects = InactiveUserManager()
    class Meta:
        proxy = True

модели запросов

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

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)

первый вариант обновить эти модели в свои команды. Это очень полезно, если эти модели изменяются только одной или двумя командами.

forms.py

class ActivateUserForm(forms.Form):
    # see above

    def execute(self):
        # see above
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()

лучше вариант будет использовать пользовательские сигналы. Эти сигналы, конечно, излучаются вашими командами. Сигналы имеют то преимущество, что вы можете синхронизировать несколько моделей запросов с исходной моделью. Кроме того, обработка сигналов может быть выгружена в фоновые задачи, используя сельдерей или аналогичные рамки.

signals.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])

forms.py

class ActivateUserForm(forms.Form):
    # see above

    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)

models.py

class InactiveUserDistribution(models.Model):
    # see above

@receiver(user_activated)
def on_user_activated(sender, **kwargs):
        user = kwargs['user']
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()

держать его в чистоте

при использовании этого подхода становится до смешного легко определить, остается ли ваш код чистым. Просто следуйте этим рекомендациям:

  • содержит ли моя модель методы, которые делают больше, чем управление состоянием базы данных? Вы должны извлечь команду.
  • содержит ли моя модель свойства, которые не сопоставляются с полями базы данных? Вы должны извлечь запрос.
  • имеет ли моя модель ссылочной инфраструктуры, которая не является моей базой данных (например, Почта)? Вы должны извлечь команду.

то же самое касается представлений (потому что представления часто страдают от одной и той же проблемы).

  • мой взгляд активно управляет моделями баз данных? Вы должны извлечь команду.

некоторые Ссылки

документация Django: прокси-модели

документация Django: сигналы

Архитектура: Доменный Дизайн

Я обычно реализую слой сервиса между представлениями и моделями. Это действует как API вашего проекта и дает вам хорошее представление вертолета о том, что происходит. Я унаследовал эту практику от моего коллеги, который использует эту технику многослойности с Java-проектами( JSF), например:

models.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"

services.py

from library.models import Book

def get_books(limit=None, **filters):
    """ simple service function for retrieving books can be widely extended """
    if limit:
        return Book.objects.filter(**filters)[:limit]
    return Book.objects.filter(**filters)

views.py

from library.services import get_books

class BookListView(ListView):
    """ simple view, e.g. implement a _build and _apply filters function """
    queryset = get_books()

имейте в виду, я обычно возьмите модели, представления и услуги на уровень модуля и отделите еще дальше в зависимости от размера проекта

прежде всего,не повторяйся.

тогда, пожалуйста, будьте осторожны, чтобы не переусердствовать, иногда это просто пустая трата времени, и заставляет кого-то потерять фокус на том, что важно. Обзор Дзен питона время от времени.

взгляните на активные проекты

  • больше людей = больше нужно правильно организовать
  • the репозиторий django у них есть простая структура.
  • the pip репозиторий у них есть структура каталогов straigtforward.
  • the хранилище ткань это также хорошо, чтобы посмотреть.

    • вы можете разместить все свои модели под yourapp/models/logicalgroup.py
  • Эл.г User,Group и родственные модели могут идти под yourapp/models/users.py
  • Эл.г Poll,Question,Answer ... может yourapp/models/polls.py
  • загрузить то, что вам нужно в __all__ внутри yourapp/models/__init__.py

подробнее о MVC

  • модель данных
    • это включает в себя
    • это также включает в себя данные сеанса / cookie / cache / fs / index
  • пользователь взаимодействует с контроллером для управления моделью
    • это может быть API или представление, которое сохраняет / обновляет ваши данные
    • это можно настроить с request.GET/request.POST ...и т. д.
    • думаю пейджинговая или фильтрация тоже.
  • обновление данных в представлении
    • шаблоны принимают данные и форматируют их соответственно
    • API-интерфейсы даже ж шаблоны/вывода являются частью представления; например,tastypie или piston
    • это также должно учитывать промежуточное программное обеспечение.

воспользоваться middleware/templatetags

  • Если вам нужно выполнить некоторую работу для каждого запроса, middleware-это один из способов.
    • например, добавление меток времени
    • например, обновление метрик о посещениях страниц
    • например, заполнение кэша
  • если у вас есть фрагменты кода, которые всегда повторяются для форматирования объектов, templatetags хороши.
    • например, активная вкладка / url панировочные сухари

воспользоваться менеджеры модель

  • создания User может перейти в UserManager(models.Manager).
  • кровавые детали для экземпляров должны идти на models.Model.
  • подробности на queryset может перейти в models.Manager.
  • вы можете создать User по время, поэтому вы можете подумать, что он должен жить на самой модели, но при создании объекта у вас, вероятно, нет всех деталей:

пример:

class UserManager(models.Manager):
   def create_user(self, username, ...):
      # plain create
   def create_superuser(self, username, ...):
      # may set is_superuser field.
   def activate(self, username):
      # may use save() and send_mail()
   def activate_in_bulk(self, queryset):
      # may use queryset.update() instead of save()
      # may use send_mass_mail() instead of send_mail()

использовать формы, где это возможно

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

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

  • например yourapp/management/commands/createsuperuser.py
  • например yourapp/management/commands/activateinbulk.py

если у вас есть бизнес-логики, вы можете отделить его

  • django.contrib.auth использует backends, так же, как БД имеет бэкэнд...так далее.
  • добавить setting для бизнес-логики (например,AUTHENTICATION_BACKENDS)
  • вы могли бы использовать django.contrib.auth.backends.RemoteUserBackend
  • вы могли бы использовать yourapp.backends.remote_api.RemoteUserBackend
  • вы могли бы использовать yourapp.backends.memcached.RemoteUserBackend
  • делегировать сложную бизнес-логику на сервер
  • убедитесь в том, чтобы установить ожидание на вход/выход.
  • изменение бизнес-логики так же просто, как изменения настроек :)

бэкэнд пример:

class User(db.Models):
    def get_present_name(self): 
        # property became not deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

может станьте:

class User(db.Models):
   def get_present_name(self):
      for backend in get_backends():
         try:
            return backend.get_present_name(self)
         except: # make pylint happy.
            pass
      return None

подробнее о шаблонах проектирования

подробнее о межфазные границы

  • - это код, который вы хотите использовать на самом деле часть моделей? - > yourapp.models
  • часть кода бизнес-логики? - > yourapp.vendor
  • является ли код частью общих инструментов / библиотек? - > yourapp.libs
  • является ли код частью библиотек бизнес-логики? - > yourapp.libs.vendor или yourapp.vendor.libs
  • вот хороший: вы можете проверить свой код самостоятельно?
  • является ли разделение логичным?
    • да, хорошая :)
    • нет, у вас могут возникнуть проблемы с тестированием этих логических понятий отдельно.
  • как вы думаете, вам нужно будет рефакторинг, когда вы получите в 10 раз больше кода?
    • да, не хорошо, не хорошо, рефакторинг может быть много работы
    • нет, это просто потрясающе!

короче говоря, вы могли бы все остальное, что поможет вам; найти интерфейсы вам нужны и границы поможет вам.

Django использует слегка измененный вид MVC. В Django нет понятия "контроллер". Ближайший прокси - это "представление", которое имеет тенденцию вызывать путаницу с преобразованиями MVC, потому что в MVC представление больше похоже на"шаблон" Django.

в Django "модель" - это не просто абстракция базы данных. В некоторых отношениях он разделяет обязанности с "взглядом" Django в качестве контроллера MVC. Он содержит все поведение, связанное с экземпляром. Если этот экземпляр должен взаимодействие с внешним API как часть его поведения, то это все равно модель код. Фактически, модели вообще не требуются для взаимодействия с базой данных, поэтому вы можете представить себе модели, которые полностью существуют как интерактивный слой для внешнего API. Это гораздо более свободная концепция "модели".

в Django структура MVC, как сказал Крис Пратт, отличается от классической модели MVC, используемой в других фреймворках, я думаю, что основная причина для этого-избегать слишком строгой структуры приложения, как это происходит в других фреймворках MVC, таких как CakePHP.

в Django MVC был реализован следующим образом:

вид слоя разделяется на две части. Представления должны использоваться только для управления HTTP-запросами, они вызываются и отвечают на них. Представления общаются с остальная часть вашего приложения (формы, модели, пользовательские классы, в простых случаях непосредственно с моделями). Для создания интерфейса мы используем шаблоны. Шаблоны являются строковыми для Django, он отображает контекст в них, и этот контекст был передан в представление приложением (когда представление запрашивает).

Model layer дает инкапсуляцию, абстракцию, проверку, интеллект и делает ваши данные объектно-ориентированными (они говорят, что когда-нибудь СУБД также будет). Это не означает, что вы должны сделать огромные models.py файлы (на самом деле очень хороший совет-разделить ваши модели в разных файлах, поместить их в папку под названием "модели", сделать '__init__.py ' файл в эту папку, где вы импортируете все ваши модели и, наконец, использовать атрибут 'app_label' моделей.Класс моделей.) Модель должна абстрагировать вас от работы с данными, это сделает ваше приложение проще. Вы также должны, если требуется, создавать внешние классы, такие как" инструменты " для ваших моделей.Вы также можете использовать наследие в моделях, устанавливая "абстрактное" свойство мета-класса модели "истина".

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

Если формы не enogh для вас, затем вы должны создать свои собственные классы, чтобы сделать магию, очень хорошим примером этого является приложение admin: вы можете прочитать код ModelAmin, это на самом деле работает как контроллер. Там нет стандартной структуры, я предлагаю вам изучить существующие приложения Django, это зависит от каждого случая. Это то, что разработчики Django предназначили, вы можете добавить класс синтаксического анализатора xml, класс соединителя API, добавить сельдерей для выполнения задач, скрученный для приложения на основе реактора, использовать только ORM, сделать веб-сервис, изменить приложение администратора и многое другое... Это ваша ответственность, чтобы сделать код хорошего качества, уважать философию MVC или нет, сделать его модульным и создать свои собственные слои абстракции. Он очень гибкий.

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

PS.: используйте класс "User" из приложения auth (из стандартного django), вы можете сделать, например, профили пользователей или, по крайней мере, прочитать его код, это будет полезно для вашего случая.

Я в основном согласен с выбранным ответом (https://stackoverflow.com/a/12857584/871392), но хотите добавить опцию в разделе Создание запросов.

можно определить классы QuerySet для моделей для выполнения запросов фильтра и son on. После этого вы можете проксировать этот класс queryset для менеджера модели, как это делают встроенные классы Manager и QuerySet.

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

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

таким образом, я придумано взлом это позволяет мне отделить логику от определений модели и по-прежнему получать все намеки от моей IDE.

преимущества должны быть очевидны, но это перечисляет несколько, что я заметил:

  • определения БД остаются только что-нет логики "мусор" прилагается
  • логика, связанная с моделью, аккуратно размещена в одном месте
  • все службы (формы, отдых, представления) имеют одну точку доступа к логика
  • самое главное: мне не пришлось переписывать какой-либо код, как только я понял, что мой models.py стал слишком загроможден и должен был отделить логику прочь. Разделение является плавным и итеративным: я мог бы выполнять функцию за один раз или весь класс или весь класс models.py.

Я использую это с Python 3.4 и выше и Django 1.8 и более значительный.

app/models.py

....
from app.logic.user import UserLogic

class User(models.Model, UserLogic):
    field1 = models.AnyField(....)
    ... field definitions ...

app/logic/user.py

if False:
    # This allows the IDE to know about the User model and its member fields
    from main.models import User

class UserLogic(object):
    def logic_function(self: 'User'):
        ... code with hinting working normally ...

единственное, что я не могу понять, это как заставить мою IDE (PyCharm в этом случае) признать, что UserLogic на самом деле является моделью пользователя. Но поскольку это, очевидно, Хак, я вполне рад принять небольшую неприятность всегда указывая тип для

Django предназначен для легкого использования для доставки веб-страниц. Если вы не комфортно с этим, возможно, вы должны использовать другое решение.

Я пишу корневые или общие операции на модели (чтобы иметь тот же интерфейс) и другие на контроллере модели. Если мне нужна операция из другой модели я импортирую свой контроллер.

такого подхода мне вполне достаточно и по сложности моих приложений.

ответ Хедде является примером это показывает гибкость самого django и python.

очень интересный вопрос в любом случае!