Service layer
So, now we came to a point where our app no longer fit to vanilla MVC and we want to extract a service layer. By “Service Layer” I mean a collection of Plain Old Ruby Objects, which hold pure business logic. What kind of POROs it might be? Here’s good examples by Bryan Helmkamp.
But, anyway, the question is – where do we put them in our Rails app files structure?
We got several options here:
- Dump everything to
lib/
dir - Call them non-ActiveRecord Models and put them in
app/models/
- Use Cases and Entities (DDD approach). So
app/use_cases/
&app/entities/
- ???
Let’s go through these options one after another.
1. lib/
If class/module is not Model, View or Controller, it suppose to go to
lib/
, isn’t it? Well, this approach got some disadvantages.
When you start to extract your business logic to POROs, on a big project
you’re going to have a few dozen of them, maybe hundred(s). Pretty
soon you open your lib/
– and it scares you. Even worse when it gets
explored by a new team member.
Another thing – it’s pretty easy to loose track on what is used where and for what reason. Especially in dynamic metaprogramming-friendly language like Ruby.
So, slowly but surely, lib/
becomes “a big ball of mud”.
2. non-ActiveRecord Models
We can say that PORO, which returns data structure(or for whatever other
reason), is non-ActiveRecord model and put it in models/
Well, that’s better. Sort of. Part is in lib/
, part is in models/
. A
slightly less mess, but in two places now.
Besides, there’s a problem each time to decide, whether this PORO is a
“model” or not. And it’s even harder for others to guess, where to find
that class(in models/
or still in lib/
), when they explore your
code.
3. Use Cases & Entities
The concept of Use Cases & Entities comes from DDD and from Bob Martin’s talk “Architecture: The Lost Years”. So, the core of your system suppose be in UseCases(verbs e.g. CreateUser) and Entities(nouns e.g. User).
But the problem with this – it takes you far away from “The Rails Way”. In one case I want to use my ActiveRecord model as Entitly, but in the other case it should be a separate class with it’s corresponding Repository class. Sometimes, my Controller is pretty much the UseCase, but sometimes I want to extract a few classes from it.
Decisions, decisions. It’s all confusing. Besides, everyone in the team need to share the exact same architectural concepts (which is solved by “The Rails Way” at first place). Pretty quick the codebase can become more mess and hard to follow through, than it was before.
4. Namespaces FTW!
So, there’s another approach, which is used by every single gem and almost any other Ruby program – the namespaced classes and modules.
Disclaimer: As usual, code examples are highly synthetic and short to keep them easy to follow. Your “results may vary”.
Example with Controllers
Let’s say you got a dashboard in your app.
1 2 3 4 |
|
And you want to show a list of users on this dashboard. But user list
should be paginated, sortable, and current_user
-specific. Now, I think
it’s a good idea to create a UserList
class, which will handle all
this logic.
But where do I put this class? Is this a Service / Model / Use Case / Entity / Query Object / Policy Object? When using namespaces, the answer is – I don’t really care.
All I need to do is to create one more folder in controllers/
with the
same name as controller.
1 2 3 4 |
|
and put my UserList
class there
1 2 3 4 5 6 7 |
|
and then it can be used in DashboardController
like this
1 2 3 4 5 6 7 8 9 |
|
Note that we don’t even need to specify the namespace.
Example with Models
Pretty much the same thing with models.
1 2 3 4 5 |
|
with files structure
1 2 3 4 |
|
and possible content
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
As a result – your code is more logically organized. It would be pretty easy to guess the locations of classes while exploring code – your app will still look like a Rails app.
And you’ll never afraid to extract one more single responsibility class, knowing that it is going to be small and “local” and won’t conflict with other’s classes.
What goes to lib/
then? API wrappers for one example. The
application-independent code, or code which doesn’t belong to any model
or controller.