...

среда, 26 февраля 2014 г.

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

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

Бонус




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

http://ift.tt/1cOm8ooподдержка конечного автомата для django-моделей (из описания). Устанавливаете на модель поле FSMField и отслеживаете изменение заранее предопределенных состояний с помощью декоратора в духе receiver.


http://ift.tt/1ev8IKpпримесь для django-моделей, позволяющая обрабатывать изменения в полях модели. Предоставляет API в стиле собственных clean_*-методов модели. Делает именно то, что указано в описании.


Там же был предложен еще один подход – абстрагировать весь код, относящийся к бизнес-логике в отдельный модуль. Например, в приложении prices выделяем весь код, ответственный за обработку цен, в модуль processing. Сходно с подходом utils.py, отличается тем, что абстрагируем бизнес-логику, а не всё подряд.


В собственных проектах я в целом использую подход автора статьи, придерживаясь такой логики:



  • Код в методе модели – если код относится к конкретному инстансу модели

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

  • Код в методе формы – если код валидирует и/или предобрабатывает данные из запроса

  • Код в методе CBV – то, что относится к request и по остаточному принципу

  • В utils.py – код, не относящийся напрямую к проекту




Обсудим?

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий