Fat models, thin views, what about tasks

For too long I’ve been doing “fat views, thin models” in practice. Only very recently did I realize (through learning from the veterans) that business logic is better handled outside of the views. So I refactored a project and moved almost all of the business logic from views to models, following some of the insightful suggestions such as Where to Put Business Logic Django? | Sunscrapers , Django models, encapsulation and data integrity - DabApps and Against service layers in Django .

In this process I did come across some duplicated filtering actions in views. But the overall result is that although models.py becomes much fatter, views.py doesn’t turn out to be a lot thinner. One downside of this refactoring is that now I cannot reason about the business logic only in views.py, and I have to keep views.py and models.py side-by-side to see the whole picture.

In the same project, there’s also a non-trivial tasks.py (~1.5k loc) and I wonder what I’m going to do with it. My models.py is already well beyond 1k loc. If I move the business logic from tasks.py to models.py again, I’m worried that models.py will become too much and certain objects will become god objects.

So what do you suggest and how do you do it in practice?

One thing that I find helps to reduce the mental load of having a huge models.py file, is to break up your models into separate files.

Even on smaller projects, I usually start with:

my_app
 - models
    - __init__.py
    - model_one.py
    - model_two.py
    - etc...

I keep the model and its manager and/or queryset in the same file.

Also, in the __init__.py file, you can do something like this to make accessing the models easier:

from my_app.models.model_one import ModelOne
from my_app.models.model_two import ModelTwo

__all__ = ["ModelOne", "ModelTwo"]

That means you can still use: from my_app.models import ModelOne

By the way, the article from “sunscrapers.com” was interesting, I hadn’t read that one before. I’d like to know if their suggestion to “implement the approve() and publish() methods on the queryset/manager” is best practice? Especially to put those methods in the QuerySet - seems like a bad code smell to have a class-level queryset making changes to a specific instance.

I was under the impression that instance-level behaviour should be defined in the model class, and this is the approach that I’ve been following for a while. I think this is also recommended by James in this post: Don't use class methods on Django models (unless I’ve misunderstood something!)

In my recent projects, I’ve done the following:

  • one model per file,
  • keep the model, manager, and queryset in the same file (helps avoid circular imports),
  • instance-level actions get implemented in the model class,
  • class level actions get implemented in the manager,
  • filters and queryset-related functions go in the queryset.

Works well for me. :slight_smile:

1 Like

Thanks for sharing your experience. I like your approach. An extra benefit of breaking a huge models.py down is that it’s easier to include a sub-model in the context window of LLMs.

As for the approve() and publish() in the article, I agree with you: in this case it makes more sense to implement it as model methods.

By the way, do you tend to include the logic in tasks.py in your models?

I tend to keep my tasks.py very, very short. I don’t really know why, but I don’t like having much logic in there - I see it as only an entry point for a process to call other code.

But that doesn’t mean I’ll put all the logic in models or managers. I often end up creating entirely new modules for this kind of logic. e.g. if I have a task for fetching data from an API, I might make a fetcher.py containing a Fetcher class that does the work and returns data about success, failure, etc.

Making it more “standalone” makes sense to me. It can then be called from other places if necessary, such as a custom management command.

Not saying this is the best or right way, just that it works for me.

3 Likes

I agree with Phil’s approach, I would tend to keep the tasks lean and encapsulate the business logic in a separate class or module.

Agree with Phil as well. It’s a pattern that the few medium/big projects I worked on moved to eventually. We called the module/package for business logic operations.py or split it into a folder when it got too big. Another project called this “commands” (although I personally find this a bit confusing with Django management commands), but that’s the similar idea. I’ve heard of a team that split them in “queries” and “mutations” to distinguish read only and write business logic.

This way your view or tasks are merely responsible for handling the input, calling the function with the business logic and rendering the appropriate response.

This is a topic that has been discussed here in the past, you might want to search for some of the other threads discussing issues like this. (I’ve included some links at the bottom to help you get started.)

I’m going to share the contrary view to the prior response.

  • Models: “Fat models” for methods working with single instances of that model.
  • Managers: Methods working with multiple instances of a model and directly-related data.
  • Views: All business logic starts in views, and remains there until an identified “re-use” opportunity arises. At that point the logic is refactored into a separate function - which remains in the views.py file until the file gets “too big”.

What constitutes “too big”? Our empirical evidence has shown that code files tend to become unmanageable at about 2500 lines. So we start looking to refactor when they go above 2000.

What we don’t do is unnecessarily proliferate the number of files. We never start from the idea of “let’s create 10 100-line files just in case they grow”.

A couple other threads on this topic:

3 Likes

Really like your approach. I feel doggedly keeping business logic outside of the views can be a pain and may drag down the development progress. You approach is more flexible.

I understand where you’re coming from, and perhaps this is good advice for established development shops, but in my own experience as a learner Python/Django developer, a models.py with more than a few models can start to get overwhelming when taking the “fat model” approach.

Each model can have 3 or more classes, and each class can have sub-classes and a whole bunch of methods. From looking at some of my own code, I have a bunch of individual model files that are over 500 lies, and even that feels like a lot for me. The idea of keeping these all in one file until they reached 2000 - 2500 lines would stress me out!

Thanks, its really helpful.