5 ms·
> Avoiding "fat models" is another place where it feels more like opinion than anything to do with performance or good design So in the Java world, the general
by Alex3917 9y ago
> Avoiding "fat models" is another place where it feels more like opinion than anything to do with performance or good design
So in the Java world, the general pattern is that:
Views:
- Accept and sanitize query parameters
- Call call one or more service methods.
- Catch errors and return an appropriate error response
- Render a JSON response based on the results of the service methods if nothing goes wrong.
Service methods:
- Perform business logic
- Manage persistence
- Bubble up errors
The nice thing about this architecture is that each piece of the codebase tells a complete story about what it's doing. That is from looking at the view you can see what parameters it accepts, how they are sanitized, what service method it calls, each of the errors that can be returned, and what the 200 response looks like.
And looking at the service method we can see what business logic it performs, and what the database queries look like.
In each case there isn't any reason to look at other methods to understand the 'story' of what's happening in your app. This makes it very easy to read the codebase and audit it for correctness.
The problem with fat models is that they're not telling a story about what's actually happening in the app, e.g. looking at them doesn't tell you anything about the business logic the endpoints are performing. And what's worse, you also can't look at the views or services and know what they're doing either.
As someone who strongly prefers Python and Django over the Java ecosystem, I'll say hands down that in terms of how web app are architected they got it right and the Django people got it wrong. As far as I can tell the whole Domain Model Architecture thing seems like a bunch of bullshit that was invented to sell consulting. If the advocates of this approach can't even write a coherent Wikipedia article, it should give you a clue as to what the code ends up looking like. [1]
[1] https://en.wikipedia.org/wiki/Domain-driven_design https://en.wikipedia.org/wiki/Domain-driven_design
- tclancy 9y agoYeah, I don't disagree with that at all. I came to Django from C# after playing with Ruby on Rails a little bit and the lack of an explicit Controller in Django confused me and I think it is part of the driver behind the "fat model" approach. I like the idea of the logic for the business object being inside it and all testable on its own but I think it has its limits-- thinking about my own Django codebases, the number of class/ static methods I have on models is a code smell from me learning OOP on C# where I had to stick those methods some place.
- mabcat 9y agoIs there a good place or pattern for service methods in Django? I've got some very fat models right now, and it's a DRY improvement over having the fat in the views, but like you say it takes a lot of effort to trace what's going on.
- ralmeida 9y agoI'm not aware of architectural patterns for service methods in Django (would like to find some as well), but what I did was to somewhat mimic a Java structure. All the project is in one single app, which I unimaginatively called "core", and inside this app there's a "services" Python package (i.e. a folder with a __init__.py file inside). These have roughly one Python module (.py file) for each "category" of services. For example, there's thin layers like "user_service.py" (basically passes through to the relevant models), to more complex services like "dependency_x_integration_service.py", which connects to external service "X" and pulls some relevant data (say, user interaction datapoints), and bridges them to the models in the system.
- Alex3917 9y agoLet's say you're following the approach of breaking down your project into separate apps, so you have an app called user_accounts. This app would be a folder containing files like: views.py, services.py, models.py, test_views.py So in views.py you'd have a User class, with: A POST method that calls services.create_user(username, email_address, password) A GET method that calls services.get_user_profile(request.user) A PUT method that calls services.update_user_profile(x, y, z) A DELETE method that calls services.inactivate_user(request.user) The return value of each of these views can just be whatever services.get_user_profile(request.user) returns, rendered into JSON. Then each of the services performs whatever business logic it needs to, preferably directly in the method. But if it would be more readable split into multiple methods, then you can create some private helper methods in the services file prefixed with an underscore. You can also have a separate folder somewhere for utility functions meant to be reused across the app, e.g. get_user_emails(request.user, is_active=True, is_verified=True) Basically though each view sanitizes the data, e.g. strips XSS out of strings, makes sure booleans are actually booleans, etc. Then each service first does field-level validation with serializers, e.g. ensuring that usernames meet the appropriate requirements for usernames. Next if there is other business logic validation that needs to happen, it happens, e.g. making sure that only users with verified email addresses can perform certain actions. After that you perform the actual business logic, e.g. transforming any data. Then you perform your CRUD operation, e.g. creating a user model. And lastly you return something, e.g. returning the user model. Each endpoint and service method can be written pretty much following this pattern, which makes the codebase super readable because once you understand one endpoint you understand all of them. And the service methods are the reusable component of the architecture, so e.g. if you want the ability for admins to create users, then they are created with the exact same service method. (But called from your admin endpoints/services.)
- sbov 9y agoWe do roughly this where I work, so I broadly agree. That said, it's fairly common for the unit of reuse to be below service methods. Also, depending upon how exactly you manage transactions, another thing to look out for is making multiple non-idempotent service calls from the view - this will be an area ripe for race conditions you likely aren't testing.
- Alex3917 9y ago> it's fairly common for the unit of reuse to be below service methods. In terms of utility functions or serializers? What does that look like exactly? E.g. in our codebase service methods can call helper methods (non-reusable), utility methods (reusable), and serializers (non-reusable).