Overview
At the end of the previous section on querysets, I covered at length an attempted explanation of where to put application-specific business logic in Django's model layer. I tried to make the argument that such an explanation is difficult to understand, especially for inexperienced developers. This makes it very easy to get wrong. Even if the advice is followed correctly, many of the suggested approaches have significant drawbacks which don't become apparent until the application reaches a certain size and complexity.
It's also worth noting that even the detailed explanation of instance level vs table level vs query level logic entirely fails to cover another possibility: what if you want to perform logical operations on single or multiple instances of different models?
That last point is illustrative. The standard advanced-Django-practitioner answer to the question of "where do I put logic that doesn't fit into a model, manager or queryset?" is likely to be something along the lines of: well, maybe just stuff that code in a function somewhere.
Here's the argument that this entire chapter revolves around: what if the answer to all of the above questions was: well, maybe just stuff that code in a function somewhere?
- Need to perform a logical operation on a single model instance? Maybe just stuff that code in a function somewhere.
- Need to perform a logic operation on multiple instances of the same model? Maybe just stuff that code in a function somewhere.
- Need to transform or derive a value based on the contents of a model field or fields? Maybe just stuff that code in a function somewhere.
- Need to implement custom queryset filtering logic? Maybe just stuff that code in a function somewhere.
You get the idea.
I'm being deliberately flippant, of course. I'm not arguing for a lack of structure, I'm just arguing for a simpler structure. To revisit the points I made in the section on programming style, I'm arguing for a set of straightforward framework protocols to define and describe the behaviour of functions which encapsulate the application's business logic.
Rather than categorising implementation approaches based on their internal details (single instance, multiple instances, chainability or otherwise) let's start by categorising based on their side effects, and assign some names and rough definitions.
- Functions that are responsible for retrieving and transforming data from the database are called reader functions.
- Functions that are responsible for changing data in the database or interacting with external systems are called action functions.
These definitions aren't fully fleshed out, but they'll do for now. If it helps, think of readers as stateless and actions as stateful (that's not quite true, but it's a good rule of thumb). Strictly, actions are effectful operations that cross a boundary or mutate state anywhere, including read-only calls to external services. Reader functions are tightly bound to Django's ORM, action functions aren't necessarily.
In the following sections, we'll dig deeply into each.
Summary
Most business logic code in most applications should live in plain functions, with well-understood interfaces, that operate on model instances, querysets or plain values. Give yourself permission to reach for complex class hierarchies, code-reuse-via-inheritance, mixins, decorators and any other form of indirection only when absolutely necessary.