Sometimes it’s really hard to find out what’s going on in a Rails app by looking into the code. Especially if it’s your first look at the source. There are lots of methods in the models and controllers and you need to spend some time to understand the way how the application works. When you start to wonder how to clean up the code, you feel that moving the methods from one model/controller to another one is not quite right, it neither changes too much nor improves your codebase. If you want to refactor your app, make it more maintainable, DRY and clean - you should sit down now and extract some Service Objects from your controllers.
Assume that below is the current working tree of our app. That’s a fact, we’ve omitted most of the files, but even if we write down a full list of source files below, it’s still not easy to find out what’s going on in this app by the first look. You know that we have users, some kind of events and costs there. Most likely users can log in and this kind of requests are processed by sessions controller. That’s it, there is only one thing that you can be pretty sure by looking at these files and folders: you’re working with a Rails app.
# Standard 'Rails' working tree app/ assets/ controllers/ application_controller.rb costs_controller.rb events_controller.rb sessions_controller.rb users_controller.rb helpers/ mailers/ models/ cost.rb event.rb user.rb views/ [...]
You can make the application “first look” much more descriptive, and that’s definitely not the only advantage of using Service Objects. Compare the list above with that:
# Working tree with services app/ assets/ controllers/ application_controller.rb costs_controller.rb events_controller.rb sessions_controller.rb users_controller.rb helpers/ mailers/ models/ cost.rb event.rb user.rb services/ add_event_for_new_team_members.rb create_event_for_event_owner.rb create_event_for_team_members.rb remove_event_from_outdated_team_members.rb set_initial_event_cost_to_zero.rb views/ [...]
One look at the services folder and you know much more than in the previous version. You know we create events for users, which possibly can be formed in teams, the team members can be added or removed, the initial event cost should be zero etc.. Much better. Now let’s take a look on how we can refactor our controller by extracting a service object from one of its methods. In a normal ‘the rails way app’ we should have a ‘create’ method in our events controller, that (hmmm…?) creates an event.
# app/controllers/events_controller.rb, some code omitted class EventsController < ApplicationController def create @event = Event.new(event_params) @event.save redirect_to @event end private def article_params params.require(:event).permit(:name, :team) end end
As you’ve noticed there’s nothing special here. But as I think you predict: all the service objects listed above should possibly be included as methods in this controller. One method per service object gives us 5 extra methods only in this one events controller (there can be dozens of them - we call it a ‘fat controller disease’). To keep it clean and avoid the mess we create a PORO (Plain Old Ruby Object) and extract all the controller methods to service objects.
# app/services/CreateEventForEventOwner.rb, most of the class omitted class CreateEventForEventOwner def initialize(event:) @event = event @user = User.find(@event.owner_id) end def call if event_not_exists? create_event assign_event_to_user end end def create_event @event = Event.create(name: @event.name, team: @event.team, owner_id: @event.owner_id) end private def event_not_exists? Event.where(name: @event.name, departure_date: @event.departure_date).first.nil? end def assign_event_to_user @user.events << @event end end
The last step is to instantiate our service object in the controller and pass the event object as an argument:
# events_controller.rb, most of the method body omitted def create CreateEventForEventOwner.new(event: @event).call end
and that’s it! We can now refactor the events controller from all the other strange methods by extracting a single action to a service object line by line.
I Recommended you to read the following blog posts/articles if you want to dig in deeper into this topic: