In the previous issue I talked about how I like to use structs rather than “presenters” in a lot of cases. When I recommend people do things like this, I’m often faced with a question:
Where do I put those classes? What folder do they go in?
They go in
app/models. Everything goes in
Humans are really into categorizing things. It’s one of the issues with these meat suits we inhabit. They’re biased. We see patterns, think we’re clever for seeing them, then try to leverage them.
You don’t need to categorize every class, module and object in your system. You don’t need to put them in hierarchies. It adds nothing to your software.
Okay, maybe it doesn’t add nothing. It’s fine for objects that fill very specific purposes to be separated. For example, controllers are always invoked by framework code and must adhere to a specific API or they just don’t work. They’re hyper-specialized and will only ever do the job of responding to HTTP requests. In almost ten years of Rails work I’ve never refactored a controller into something else. It’s handy knowing that anything in your Rails app’s “controllers” folder is going to be a controller.
The rest of your system isn’t like that. Some objects represent processes, others concrete things, others are just helper objects that glue things together. When building the code that represents your business logic, don’t be so quick to categorize it.
The worst offender, I find, is services. I have deleted so many
app/services folders from projects where no one could tell me what a service actually was. Even if you do have a “service” convention on your project, I’d argue that you shouldn’t
The current top hit
on Google for “service objects rails” explains that controllers and models only get you so far before you need something else. It claims that “service objects” fill this gap, showing off an implementation that’s little more than a glorified function that uses an object instance to allow for object-oriented-looking patterns.
I personally prefer service objects that take dependencies to their constructor and parameters to their call method, but that’s neither here nor there. Service objects are just fancy functions. Would you make an
app/functions folder? What would that tell you about the objects in that folder that you want to know?
I typically point to this article
when prying service objects out of a team’s hands. I largely agree with Avdi’s sentiment. We are object-oriented programmers and have rich patterns for representing our application’s domain model in code. We shouldn’t be jamming all our logic into “services” anymore than putting everything in controllers or models.
Once teams admit that the structure of their code is supposed to reflect the behaviour of the system and the domain, it’s easy to get away from overusing the service object pattern, but that just brings us back to the question of where to put all our heterogenous objects.
I’ve seen teams pick
app/insert_app_name, and more. None of those are all that terrible, but it’s just not a distinction worth making: just put it in
app/models. Sure, it takes a little stretch to go for “that’s where my database models go” to “that’s where my domain model (which includes my database models) goes”, but it’s not that big of a leap.
Database models already (mostly) represent the thing kinds of things that your domain is concerned with. I say “mostly” because it’s not always strictly true; a join table might represent something you have a name for, for example. The same should be true of your other objects. They should mostly represent ideas, processes, concepts, actions, objects, quantities and whatever else your application is concerned with.
Besides a handful of ActiveRecord methods like
save, database models don’t respond to any more consistent an API than the rest of your objects. Functionally, the bulk of their APIs are just as heterogeneous as any other object in the system, full of associations, columns and custom methods.
Yes, I can see some limited value in keeping database models separate from everything else. In practice it’s not a real problem. I have no interest in answering the question, “how many
ActiveModel mixins do you have to include in a class before it behaves enough like a database model that it goes in
Code is fluid. What’s a service today might be a struct tomorrow. By Friday it might be an
ActiveModel::Model. Next week it might become a rich domain object that ties state and behaviour together around a single responsibility. Ideally it will, so let’s not make that path any harder.