The conceit that fat controllers pose problems for MVC web applications, specifically, is not a new one. It was popularized, at least, on the Interwebs by Jamis Buck who was writing about Rails apps.
Updating the paradigm specifically for Django, the problem is excessive logic in templates (which are the analog to the traditional view later), which needs to be solved by moving that logic to the controllers (in Django the analog is views), which then needs to be solved by moving this logic to the models (same terminology!).
We’re going to skip over the mild controversy about terminology and alternatives and just ride with this for the moment. What I want to show you of is that much of this is problematic when too much is encoded in your model classes and for superior maintainability and flexibility.
Let’s talk querysets, specifically custom querysets (and manager methods, generally). They allow for composable, testable, and human readabl logic for crafting good queries to read model from the database.
Generally speaking, they provide an excellent way of encapsulating functionality that needs to deal with multiple rows from the database at once.
Here’s a simple example: the app requires a dropdown menu based on some data in the database. No biggie, this is just a ModelChoiceForm. Except we need the data stored to be human readable, too, not primary table keys. In fact, we want to use the slug values for the stored choices - but again, this is not the primary key. And it’s possible this list needs to differ depending on the user.
Well, we could add some logic to the form class to handle this, or perhaps a custom field… or since it’s only the resultant data we care about and not the replicated relationships, we can use a regular ChoiceField and get our choices from a queryset method, like so:
class SluggedQueryset(models.QuerySet): def slugchoices(self): for t in self: yield t.slug, t.name
This wasn’t a hard problem, by any means, but considered as an avatar of similar scenarios, the result:
options = forms.ChoiceField(choices=MyModel.objects.active().slugchoices())
… is a simple and effective improvement that keeps the data logic close to the data source.
Manager methods in general aren’t leveraged enough for the mutable elements of the CRUD acronym - and they make up 3/4th!
The create method itself can often be extended to include creation-relevant functionality that otherwise gets lumped into views, for example setting default values conditionally or ensuring that related objects are created at the same time.
Customized update methods can be used to send signals - something the default update method does not do - or directly trigger outside actions (e.g. sending notifications). To be clear, a customized update method that does something like making calls to external services should probably be an additional method rather than an extended version of the base update method. There is nothing wrong with updating or replacing core methods, per se, but once you get into significant side effects, especially outside the app, it’s best to use special case methods.
And here’s the thing about updating existing code by using fat managers. Once you start looking specifically for these patterns, in views, forms, admin class, even model class, often the code blocks start to jump out. It looks like a line here, a line there, and pretty soon you can start slimming down blocks of code all over the place, making it more testable, more readable, and often by virtue of the specific replacements, more reliable.
Learn from more articles like this how to make the most out of your existing Django site.