This week I watched Alex Gaynor's recent talk entitled "The end of MVC" on Youtube:
To summarise, I think Alex's main point was that developers tend to weave business logic through each of the three MVC components rather than cleanly separating business logic from the other concerns of writing a web application: persistence, templating, request/response parsing and so on.
Given a limited set of framework components to hang things on, developers just bodge logic into that framework wherever it is most convenient, forgetting how to write self-contained, well-organised Python business logic that they might write if the framework was absent.
As a symptom of this, I've interviewed candidates who, when presented with a simple OO business logic exercise, start by writing Django models. Please don't do this.
As someone who has done Django projects at a range of scales over the past 8 years this talk chimed with the lessons I myself have learned about how to organise a project. I've also picked up other people's Django projects here and there, and suffered the pain of poor organisation. To be fair, the situation isn't as bad with Django as I remember it being when I picked up PHP projects 10 years ago: there is always a familiar skeleton provided by the framework that helps to ensure a new code base is moderately accessible.
I tried a long time ago to write up some thoughts on how to organise "controllers" (Django views) but re-reading that blog post again now, I realise I could have written it a lot better.
So to turn my thinking into practical recommendations (Django-specific, adapt as required):
Keep database code in models.py
It stinks when you see cryptic ORM queries like
randomly plastered throughout your application. Keeping these with the model definitions makes them more maintainable.
- Keep ORM queries in models.py
- Learn how to define custom managers, queryset subclasses etc as appropriate
- Encapsulate the object queries you need using these features
- Don't put business logic here.
Naturally, this generalises to other databases and persistence layers. For example, I did a very successful project with ElasticSearch, and all queries were implemented in searches.py; a small amount of user state was persisted in Redis, and there was a module that did nothing more than encapsulating Redis for the operations we needed.
Keep presentational code in templates
- Write template tags and filters to add power to the template syntax rather than preparing data for presentation elsewhere.
- Don't put ORM queries in templates. Make sure that your template parameters include all the things that will be necessary for rendering a page.
- Don't put business logic here (obviously)
Writing better template tags and filters empowers you to change your presentation layer more easily, while maintaining a consistent design language across the site. It really feels great when you just drop a quick tag into a template and the page appears great on the first refresh, because your template tags were organised well to display the kinds of thing your application needs to show.
Override forms to do deep input pre-processing
- Build on the forms system to push all of your validation code in forms.py, not just input conversion and sanitisation.
Forms are an explicit bridge to adapt the calling conventions of HTML to the calling conventions of your business logic, so make sure they fully solve this task.
As an example, your application might want validate a complex system of inputs against a number of database models. I would put all of this logic into the form and have the form return specific database models in cleaned_data.
That's not to say that some abstract parts of the code should not be extracted to live somewhere else, but the big advantage of Django forms is that errors can be faithfully tied back to specific inputs.
Create your own modules for business logic
- Write your business logic in new modules, don't try to include it with a Django framework component.
- Try to make it agnostic about persistence, presentation etc. For example, use dependency injection.
Views should just contain glue code
- What is left in a view function/class should be just glue code to link requests to forms, forms to business logic, outputs to templates.
- The view then serves as a simple, straightforward description of how a feature is coupled.