📜 ⬆️ ⬇️

Organizing code in django applications or thick models is great.

From translator
As always free translation of an interesting article about a specific approach to code organization in django-applications. It will be useful:
  • Those who have not thought about such issues
  • Those who already have their own views on the organization of logic, but not against the alternative options
  • Those who already use the discussed approach to confirm their thoughts
  • Those who no longer use the discussed approach and have arguments against

There won't be a lot of code, the article is mostly debatable. Enzhoy)


image
Thick models.

Intro


Most django tutorials and examples on this framework set a bad example for beginners in terms of organizing code in their projects. In the case of a large / long-playing application, the code structure, underlined from similar examples, can cause nontrivial problems and difficult situations in the development process. In this article I will describe an alternative, rather rare approach to code organization, which I hope will seem interesting to you.

MVC in django = MTV + embedded C


Have you ever tried to explain how MTV is organized in django, say, RoR-developer? Some might think that templates are views, and views are controllers. Not certainly in that way. A controller is a URL router built into django that provides request-response logic. Views are needed to present the right data in the right templates. Templates and views collectively constitute the “presentation” layer of the framework.
')
There are many advantages in such MTV - with its help, you can easily and quickly create typical and not only applications. However, it remains unclear where the logic for processing and editing data should be stored, where to abstract the code in which cases. Let's evaluate several different approaches and look at the results of their application.

Logic in views


Put all or most of the logic in the view. The approach most often found in various tutorials and for beginners. It looks something like this:

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) 


It is extremely simple, so at first glance it is attractive - all the code is in one place, there is no need to strain the brain and abstract something there. But this is only at first glance. Such an approach does not scale well and quickly leads to loss of readability. I had the good fortune to contemplate ideas for half a thousand lines of code, from which even experienced developers had reduced cheekbones and cams. Thick views lead to duplication and complication of the code, they are hard to test and debug, and as a result, it is easy to break.

Logic in Forms


Forms in django are object-oriented, they validate and clear data, so that they can also be considered as the location of logic.

 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) 


Already better. The problem is that now the form for receiving payment is also engaged in processing commissions for credit cards. Nekomilfo. What if we want to use this function in some other place? We are, of course, smart and could code the necessary impurities, but again, what if we need this logic in the console, in celery, or another external application? The decision to instantiate the form to work with the model does not look right.

Code in class-based views


The approach is very similar to the previous one - the same advantages, the same disadvantages. We do not have access to logic from the console and from external applications. Moreover, the inheritance pattern of the views in the project is complicated.

utils.py


Another simple and tempting approach is to abstract all side code from views and put it in the form of utility functions into a separate file. It would seem that a quick solution to all problems (which many in the end and choose), but let's think a little.

 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) 


The first problem is that the location of such functions may not be obvious, the code relating to certain models can be spread over several packages. The second problem is that when your project grows up and new / other people start working on it, it will be unclear what functions exist at all. That, in turn, increases development time, annoys developers and can cause duplication of code. Certain things can be caught on the code review, but this is a decision after the fact.

Solution: fat models and fat managers


Models and their managers are an ideal place to encapsulate code for processing and updating data, especially when such code is logically or functionally tied to the capabilities of the ORM. In essence, we extend the API of the model with our own methods.

 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) 


For my taste, this solution is the most correct. The credit card processing code is gracefully encapsulated, the logic is in a relevant place for it, the necessary functionality is easy to find and (re) to use.

Summary: General Algorithm


Where to write code ble @ th ? If your logic is tied to the request object, then it is probably the place in the view. Otherwise, consider the following order of options:

If none of the options fit, it may be worthwhile to consider abstracting into a separate utility-function.

TL; DR


The logic in the models improves django-applications not to mention your hair .

Bonus


In the comments to the original topic slipped links to two interesting applications that are close to the topic of the article.

github.com/kmmbvnr/django-fsm - state machine support for django-models (from the description). You set the FSMField field on the model and track the change of the predefined states with the help of a decorator in the spirit of the receiver .

github.com/adamhaney/django-ondelta is an impurity for django models that allows you to process changes in the fields of a model. Provides API in style of own clean _ * - methods of model . Does exactly what is stated in the description.

In the same place, another approach was proposed - to abstract all code related to business logic into a separate module. For example, in the prices application, select the entire code responsible for processing prices in the processing module. Similar to the utils.py approach, it differs in that we abstract business logic, and not everything in a row.

In my own projects, I generally use the approach of the author of the article, adhering to this logic:

Discuss?

Source: https://habr.com/ru/post/213875/


All Articles