Keep your controllers clean - use Service Objects

2015, Aug 01    

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:

  1. http://blog.arkency.com/2013/09/services-what-they-are-and-why-we-need-them/
  2. http://hawkins.io/2014/03/rethinking-application-architecture-talk/
  3. https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services
  4. http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  5. https://netguru.co/blog/service-objects-in-rails-will-help