My first Service Objects in Ruby on Rails

I am learning how to keep my controllers clean as they grow while I develop. I am also trying to make my controllers handle only the HHTP-related stuff and move any other logic outside of it to make it read better and optimize it.

As I understand it, a Service Object implements all kind of logics in the controller unrelated to HTTP stuff (params, render, redirect). In fact, services help understand the programmer what the application really does, which is not always obvious when looking at controllers or models. Services have the benefit of extracting the core logic of the application to an isloated object, instead of scattering it around controllers and models.

Let’s see an example. In my application I create reviews for movies. Additional requirements is that the review can have the rating of 0-10 and this can be achieved by skipping the rating in the form for a new review. In order to handle that I need to check if the value of the rating provided in the form in params is blank, if it is I assign the rating of 0 to the review.

class ReviewController < ApplicationController
  ...

  def create
    @review = current_user.reviews.build(review_params)
    @review.movie_id = @movie.id
    @review.user_id = current_user.id

    if params[:review][:rating].blank?
      @review.rating = 0
    end

    if @review.save
      redirect_to movie_path(@movie)
      @review.create_activity :create, owner: current_user
    else
      render 'new'
    end

  end

  ...

end

This method seems rather long. It could be easily refactored, especially the place where I check if the rating is present in params. Let’s refactor this part and make the code a little bit cleaner.

First, we will create a new folder Services in the root folder of our application. Inside it we will create a new file create_review_service.rb. In create_review_service.rb we will create CreateReviewService class. It’s a simple class, it does not inherit from ApplicationController this time because we are isolating everything that happens in the controller
from non-HTTP-related stuff. What we also need is the private method #find_movie that finds the movie we are creating the review for.

class CreateReviewService

  def call(movie_id, attributes, user)
    review = user.reviews.build(attributes)
    movie = Movie.find(movie_id)
    review.movie = movie

    if attributes[:rating].blank?
      review.rating = 0
    end

    review.save
    review
  end

  private
    def find_movie(attributes)
      Movie.find(attributes[:id])
    end
end

Now the controller looks like this:

class ReviewsController < ApplicationController
  ...

  def create
    @review = CreateReviewService.new.call(
      params[:movie_id], review_params,  current_user
    )

    if @review.valid?
      @review.create_activity :create, owner: current_user
      redirect_to movie_path(@movie)
    else
      render 'new'
    end
  end

  ...

end

I create a new object CreateReviewService inside #create method and call #call method on it and pass necessary parameters to the Service Object, where the actual Review is created. Once the Service Object is created, it contains the Review (whether valid or not – validations still work here!) so the only thing that I need to do is to check whether it’s valid.

This code can be refactored further. As you see, after the review is created I create a new activity for a review. This could be a new service.

class CreateActivityForReview
  def call(review, user)
    review.create_activity :create, owner: user
  end
end

I could now create new service object in the controller but I prefer to call it inside of another service – CreateReviewService. Service Object can be called from another Service Object as well.

class CreateReviewService

  def call(movie_id, attributes, user)
    review = user.reviews.build(attributes)
    movie = Movie.find(movie_id)
    review.movie = movie
    if attributes[:rating].blank?
      review.rating = 0
    end
    review.save
    CreateActivityForReview.new.call(review, user)
    review
  end

  private
    def find_movie(attributes)
      Movie.find(attributes[:id])
    end
end

Now, my controller looks like this:

class ReviewController < ApplicationController
  ...
  def create
    @review = CreateReviewService.new.call(params[:movie_id],
    review_params, current_user)
    if @review.valid?
      redirect_to movie_path(@movie)
    else
      render 'new'
    end
  end
end

A book that broadly and comprehensively describes the idea of Service Objects is Fearless Refactoring: Rails controllers by Andrzej Krzywda. It presents numerous real life refactoring examples and techniques. You can see some sample code at http://rails-refactoring.com/. I really recommend reading it, it’s an awesome book. A must read!

Thank you for reading!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s