The problem of Rails controller actions growing bigger over time has been discussed many times, and refactoring
controller actions is not a secret for anyone. I want to share my approach, which combines a few approaches I have
learned from various developers with some of my personal researches.
Basically, every refactoring of controller action consists of only one significant step - ‘Extract service object’.
In my opinion, service objects are applicable when the extracted code is responsible for interaction with third parties,
background queue managers, i.e performing some actions that have either success or failure status. The concept of
interactors is perfectly applicable here.
My approach differs from the conventional service object approach because, in my controllers, service objects are supposed
to return another object, and I already have seen some articles on the web, where people use “Result object” term, but
I prefer to call it “View object”. View object has nothing to do with cells,
it consists of local variables, template and layout names, and status codes.
Here are the steps to refactor:
Inline controller callbacks
Change all instance variables to local variables
Extract all logic, except cookies/session manipulation and redirects to separate class
In newly created separate class, split querying logic into descriptive methods
Return object, responding to status, locals, template, layout methods, the last two are very optional
My credits to Arkency’s Andrzej Krzywda, because I got some ideas from his brilliant
Fearless Refactoring: Rails Controllers book, and to Ivan Shamatov, which has written
this inspiring article.
Let’s start with example controller:
This example controller seems not to be very complicated, but it’s fat enough and should be refactored.
Inline controller callbacks
We simply remove before_action line and made it inline, inserting respective method call in the controller action body.
Change all instance variables to local variables
You also have to change all instance variables in the templates to the local ones, but this step thoroughly described
in Fearless Refactoring book, so I would not touch it here.
Extract all logic, except cookies/session manipulation and redirects to separate class
For this step I prefer the following naming convention: make a separate directory for this kind of classes under app/services.
Then namespace all the classes exactly like they are namespaced in respective controllers, for this example,
the full path to the file will be app/services/view_objects/desktop/opinions/index.rb
And controller becomes only this:
Much better, but we have one bloated class for another, and this is the time for the next step.
In newly created separate class, split querying logic into descriptive methods
Here we just extracted methods from the call method, but we also should extract querying logic from the opinions method:
Return object, responding to status, locals, template, layout methods, the last two are very optional
Our so-called view object is already responding to all of these messages, but we can make a better use of the status
message. The raise being hidden in the private section of the service class adds implicitness to overall control flow,
and thus we can bring it back to the controller with the use of the status. I tend to leave in controller actions
all the things, that directly affect render/redirect cycle, and in other controllers, we can have more complicated
logic, more options for conditional redirects.
And finally, controller action turns into this:
Also. instead of status codes, you may prefer to raise custom errors inside the interactor/service classes, and then
rescue them in controllers, but my personal preference is to use as fewer rescues as possible because rescue in ruby
is known for being slow.