Я пишу проект в Django, и я вижу, что 80% кода находится в файле models.py
. Этот код запутан, и через некоторое время я перестаю понимать, что на самом деле происходит.
Вот что меня беспокоит:
User
, но технически они должны создавать их равномерно.Вот простой пример. Сначала модель 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])
Я хочу отделить объекты в моем коде:
Каковы наилучшие методы реализации такого подхода, который может быть применен в Django?
Похоже, вы спрашиваете о различии между моделью данных и моделью домена - последняя - это то, где вы можете найти бизнес-логику и сущности, воспринимаемые вашим конечным пользователем, первая - это то место, где вы фактически храните свои данные.
Кроме того, я интерпретировал третью часть вашего вопроса как: как заметить отказ в том, чтобы держать эти модели в отдельности.
Это две совершенно разные концепции, и всегда трудно их разделить. Однако существуют некоторые общие шаблоны и инструменты, которые можно использовать для этой цели.
Первое, что вам нужно признать, это то, что ваша модель домена не имеет отношения к данным; речь идет о действиях и таких вопросах, как "активировать этого пользователя", "деактивировать этого пользователя", "какие пользователи в настоящее время активированы?" и "что это за имя пользователя?". В классических терминах: это о запросах и командах.
Давайте начнем с рассмотрения команд в вашем примере: "активировать этого пользователя" и "деактивировать этого пользователя". Самое приятное в командах состоит в том, что они легко могут быть выражены небольшим сценарием "когда-то":
данный неактивный пользователь
, когда администратор активирует этого пользователя
, затем пользователь становится активным
и отправляется подтверждение электронной почты пользователю
и в системный журнал добавляется запись
(и т.д.).
Такой сценарий полезен для того, чтобы увидеть, как разные части вашей инфраструктуры могут быть затронуты одной командой - в этом случае ваша база данных (какой-то "активный" флаг), ваш почтовый сервер, системный журнал и т.д.
Такой сценарий также действительно поможет вам в создании тестовой среды разработки.
И, наконец, мышление в командах действительно помогает вам создать ориентированное на задачи приложение. Ваши пользователи оценят это: -)
Django предоставляет два простых способа выражения команд; они оба являются действительными вариантами, и нет ничего необычного в смешивании двух подходов.
Сервисный модуль уже был описанный @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 Form для каждой команды. Я предпочитаю этот подход, потому что он сочетает в себе множество тесно связанных аспектов:
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)
Конечно, это работает только в том случае, если данные действительно представлены в вашей модели данных. Это не всегда так. В этих случаях вы можете рассмотреть варианты ниже.
Первый вариант полезен для запросов, которые являются просто презентационными: пользовательские теги и фильтры шаблонов.
template.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
Прокси-модели очень полезны в контексте бизнес-логики и отчетности. В основном вы определяете расширенный поднабор вашей модели.
models.py
class InactiveUserManager(models.Manager):
def get_query_set(self):
query_set = super(InactiveUserManager, self).get_query_set()
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()
Лучшим вариантом было бы использование пользовательских сигналов. Ваши сигналы, конечно, испускаются вашими командами. Преимущество сигналов состоит в том, что вы можете синхронизировать несколько моделей запросов с исходной моделью. Кроме того, обработка сигналов может быть выгружена в фоновые задачи, используя Celery или аналогичные структуры.
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()
При использовании этого подхода становится нелепо легко определить, остается ли ваш код чистым. Просто следуйте этим рекомендациям:
То же самое относится к представлениям (поскольку представления часто страдают от одной и той же проблемы).
Обычно я использую сервисный уровень между представлениями и моделями. Это действует как ваш 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()
Помните, я обычно беру модели, представления и службы на уровень модуля и отделить еще больше в зависимости от размера проекта
Прежде всего, Не повторяйте.
Тогда, пожалуйста, будьте осторожны, чтобы не перерабатывать, иногда это просто пустая трата времени и заставляет кого-то потерять сосредоточение на том, что важно. Периодически проверяйте zen of python.
Взгляните на активные проекты
репозиторий ткани также является хорошим, на что можно смотреть.
yourapp/models/logicalgroup.py
User
, Group
и связанные модели могут находиться под yourapp/models/users.py
Poll
, Question
, Answer
... может находиться под yourapp/models/polls.py
__all__
внутри yourapp/models/__init__.py
request.GET
/request.POST
... и т.д.tastypie
или piston
Воспользуйтесь middleware/templatetags
Воспользуйтесь менеджерами моделей
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
использует бэкэнды, так же как db имеет бэкэнд... и т.д.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
Короче говоря, вы могли бы
yourapp/core/backends.py
yourapp/core/models/__init__.py
yourapp/core/models/users.py
yourapp/core/models/questions.py
yourapp/core/backends.py
yourapp/core/forms.py
yourapp/core/handlers.py
yourapp/core/management/commands/__init__.py
yourapp/core/management/commands/closepolls.py
yourapp/core/management/commands/removeduplicates.py
yourapp/core/middleware.py
yourapp/core/signals.py
yourapp/core/templatetags/__init__.py
yourapp/core/templatetags/polls_extras.py
yourapp/core/views/__init__.py
yourapp/core/views/users.py
yourapp/core/views/questions.py
yourapp/core/signals.py
yourapp/lib/utils.py
yourapp/lib/textanalysis.py
yourapp/lib/ratings.py
yourapp/vendor/backends.py
yourapp/vendor/morebusinesslogic.py
yourapp/vendor/handlers.py
yourapp/vendor/middleware.py
yourapp/vendor/signals.py
yourapp/tests/test_polls.py
yourapp/tests/test_questions.py
yourapp/tests/test_duplicates.py
yourapp/tests/test_ratings.py
или что-нибудь еще, что поможет вам; поиск интерфейсов, которые вам нужны, а границы помогут вам.
Django использует слегка модифицированный вид MVC. В Django нет понятия "контроллер". Самый близкий прокси - это "представление", которое, как правило, вызывает путаницу с конвертируемыми MVC, потому что в MVC вид больше похож на "шаблон" Django.
В Django "модель" - это не просто абстракция базы данных. В некоторых отношениях он разделяет обязанности с "представлением" Django в качестве контроллера MVC. Он содержит всю совокупность поведения, связанного с экземпляром. Если этому экземпляру необходимо взаимодействовать с внешним API как часть его поведения, то этот код все еще моделируется. На самом деле, модели не обязаны вообще взаимодействовать с базой данных, поэтому вы могли бы иметь модели, которые полностью существуют как интерактивный уровень для внешнего API. Это гораздо более свободная концепция "модели".
В Django структура MVC, как сказал Крис Пратт, отличается от классической модели MVC, используемой в других рамках, я думаю, что основная причина для этого - избежать слишком строгой структуры приложения, например, происходит в других средах MVC, таких как CakePHP.
В Django MVC был реализован следующим образом:
Вид слоя разделен на два. Представления должны использоваться только для управления HTTP-запросами, они вызываются и отвечают на них. Представления взаимодействуют с остальной частью вашего приложения (формы, модели, пользовательские классы, в простых случаях непосредственно с моделями). Для создания интерфейса мы используем Шаблоны. Шаблоны похожи на Django, они сопоставляют контекст с ними, и этот контекст был передан приложению приложением (при запросе вида).
Уровень модели предоставляет инкапсуляцию, абстракцию, проверку достоверности и делает ваши данные объектно-ориентированными (они говорят, что когда-нибудь СУБД также будет). Это не означает, что вы должны создавать огромные файлы models.py(на самом деле очень хороший совет - разделить ваши модели в разных файлах, поместить их в папку под названием "модели", сделать файл "__init__.py" в этом где вы импортируете все свои модели и, наконец, используете атрибут "app_label" для моделей. Класс модели). Модель должна абстрагировать вас от работы с данными, это упростит ваше приложение. Вы также должны, при необходимости, создавать внешние классы, такие как "инструменты" для своих моделей. Вы также можете использовать наследие в моделях, устанавливая атрибут "abstract" вашей модели Meta-класса на "True".
Где остальные? Ну, небольшие веб-приложения, как правило, представляют собой своего рода интерфейс к данным, в некоторых небольших программных случаях достаточно использовать представления для запроса или вставки данных. Более распространенные случаи будут использовать Forms или ModelForms, которые на самом деле являются "контроллерами". Это не что иное, как практическое решение общей проблемы и очень быстрая. Это то, что использует веб-сайт.
Если формы для вас не нужны, тогда вы должны создать свои собственные классы, чтобы сделать магию, очень хорошим примером этого является приложение администратора: вы можете прочитать код ModelAmin, это фактически работает как контроллер. Существует не стандартная структура, я предлагаю вам изучить существующие приложения Django, это зависит от каждого случая. Это то, что намеревались разработчики Django, вы можете добавить класс парсера xml, класс соединителей API, добавить Celery для выполнения задач, скрутить для приложения на базе реакторов, использовать только ORM, сделать веб-сервис, изменить приложение администратора и многое другое... Это ваша ответственность, чтобы сделать код хорошего качества, уважать философию MVC или нет, сделать его модульным и создать свои собственные уровни абстракции. Он очень гибкий.
Мой совет: прочитайте как можно больше кода, есть много приложений django, но не воспринимайте их так серьезно. Каждый случай отличается, шаблоны и теория помогают, но не всегда, это неточный язык, django просто предоставляет вам хорошие инструменты, которые вы можете использовать, чтобы смягчить некоторые боли (например, интерфейс администратора, проверку веб-формы, i18n, реализацию шаблона наблюдателя, все ранее упомянутые и другие), но хорошие проекты исходят от опытных дизайнеров.
PS: используйте класс "Пользователь" из приложения auth (из стандартного django), вы можете сделать, например, профили пользователей или, по крайней мере, прочитать его код, это будет полезно для вашего случая.
Старый вопрос, но я все равно хочу предложить свое решение. Это основано на признании того, что объектам модели также требуются некоторые дополнительные функции, в то время как неудобно размещать их в пределах models.py. Тяжелая бизнес-логика может быть написана отдельно в зависимости от личного вкуса, но я, по крайней мере, как модель, делаю все, что связано с ней. Это решение также поддерживает тех, кто любит иметь всю логику, размещенную внутри самих моделей.
Как таковой, я разработал хак, который позволяет мне отделять логику от определений моделей и все еще получать все намеки от моей IDE.
Преимущества должны быть очевидны, но в этом перечислены некоторые из них, которые я наблюдал:
Я использовал это с Python 3.4 и выше и Django 1.8 и выше.
приложение/models.py
....
from app.logic.user import UserLogic
class User(models.Model, UserLogic):
field1 = models.AnyField(....)
... field definitions ...
приложение/логика/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 на самом деле является моделью пользователя. Но поскольку это, очевидно, хак, я очень рад принять небольшую неприятность, всегда указывающую тип для параметра self
.
Я в основном согласен с выбранным ответом (https://stackoverflow.com/questions/12578908/separation-of-business-logic-and-data-access-in-django), но хочу добавить опцию в раздел "Создание запросов".
Можно определить классы QuerySet для моделей для запросов фильтра и сына. После этого вы можете проксировать этот класс запроса для диспетчера модели, например, для классов сборки и QuerySet.
Хотя, если вам пришлось запрашивать несколько моделей данных для получения одной модели домена, мне кажется более разумным поставить это в отдельный модуль, как это было предложено ранее.
Django предназначен для простого использования для доставки веб-страниц. Если вы не можете с этим справиться, возможно, вы должны использовать другое решение.
Я пишу корневые или общие операции над моделью (чтобы иметь тот же интерфейс), а остальные - на контроллере модели. Если мне нужна операция из другой модели, я импортирую ее контроллер.
Этот подход достаточно для меня и сложности моих приложений.
Ответ Hedde - это пример, демонстрирующий гибкость самого django и python.
Очень интересный вопрос в любом случае!