Keep you controllers clean - use Service Objects

| Comments

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 on the source. There are lots of methods in the models and controllers and actually 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 maintanable, 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.

Your Current working tree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 definetely not the only advantage of using Service Objects. Compare the list above with that:

Your Current working tree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 it’s 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

CreateEventForEventOwner.rb Servie Object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CreateEventForEventOwner
  #most of the class omitted

  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:

create method in events_controller.rb
1
2
3
4
def create
  #most of the method body omitted
  CreateEventForEventOwner.new(event: @event).call
end

and that’s it! We can now refeactor 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

Comments