Keep your controllers clean - use Service Objects
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:
- http://blog.arkency.com/2013/09/services-what-they-are-and-why-we-need-them/
- http://hawkins.io/2014/03/rethinking-application-architecture-talk/
- https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services
- http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
- https://netguru.co/blog/service-objects-in-rails-will-help