DEV Community

Cover image for What are Service Objects in Ruby on Rails? Should you use it?
Aweys Ahmed
Aweys Ahmed

Posted on

What are Service Objects in Ruby on Rails? Should you use it?

In a previous blog post, I mentioned that it was considered good practice to keep your controllers skinny and add the bloat to your model. However, this is not always desirable.

Using service objects we can try and keep both our controller and model lean, readable and easier to test. This will make everyone happy.

What are Service Objects?

Service Objects are plain old Ruby Objects (PORO). They are created to remove the bloat from your controller and/or model.

It is intended to move business logic into separate files that you can be used elsewhere.

Service objects should adhere to the single responsibility principle. This means that your service object should focus on doing one thing.

It is important that your service object method returns something substantial and meaningful like an object.

Let's look at an example of code that needs to be refactored. In the example below, we'll have an Employee's controller that renders the employees details but what we'll be focusing on is the calculation of federal and provincial taxes (This is not based on actual tax rates in Canada).

class EmployeesController < ApplicationController
  before_action :set_employee, only: [:show, :edit, :update, :destroy]

  # GET /employees/1
  # GET /employees/1.json
  def show
    @federal_tax = @employee.salary / 10
    @provincial_tax = @employee.salary / 20
  end
end
Enter fullscreen mode Exit fullscreen mode

So you can see that we are calculating the tax rates and storing them in an instance variable to be rendered in the show view. This works but isn't optimal.

Let's see how service objects can help us.

How do we use a Service Object?

The first thing we need to is to create a services folder under the app directory. Rails will autoload anything under the app directory.

Then we will create a class called ApplicationService that our service objects will inherit from.

class ApplicationService
  def self.call(*args, &block)
    new(*args, &block).call
  end
end
Enter fullscreen mode Exit fullscreen mode

This above class method creates a new instance of the class with the arguments that are passed into it and calls the call method on the instance.

(WHATTTTTT?)

Let's look at an example to make sense of what that means.

Let's create a service object that converts kilometres into meters.

class KilometresToMeters
  attr_reader :measurement

  def initialize(measurement)
   @measurement = measurement
  end

 def call 
  measurement  * 1000
 end
end 
Enter fullscreen mode Exit fullscreen mode

If we didn't inherit from the ApplicationService, this is what our service object would look like in our controller.

def calculate
 @distance = KilometresToMeters.new(params[:value]).call
end
Enter fullscreen mode Exit fullscreen mode

Here is the same call using the ApplicationService inheritance. Since we created an instance of the class in our ApplicationService, we don't need to do it when we use the service object in the controller.

def calculate
 @distance = KilometresToMeters.call(params[:value])
end
Enter fullscreen mode Exit fullscreen mode

It isn't the biggest change but I prefer this way of shortening the service object call and it increases the readability of the code.

Now let's implement our service objects for our tax calculator.

class FederalTaxAmount < ApplicationService
 attr_reader :salary

 def initialize(salary)
  @salary = salary
 end

 def call
  salary / 10
 end
end
Enter fullscreen mode Exit fullscreen mode

Let's repeat the process for the provincial tax calculation.

# frozen_string_literal: true

class ProvincialTaxAmount < ApplicationService
  attr_reader :salary

  def initialize(salary)
    @salary = salary
  end

  def call
    @salary / 20
  end
end
Enter fullscreen mode Exit fullscreen mode

Now that we have created our service objects, let's see what they will look like in the controller.

  def show
    @federal_tax = FederalTaxAmount.call(@employee.salary)
    @provincial_tax = ProvincialTaxAmount.call(@employee.salary)
  end
Enter fullscreen mode Exit fullscreen mode

This controller looks cleaner than what we used in the beginning. This was a simple example for illustration purposes but you can imagine how this could clean up a larger and more complex controller.

The service objects have also made it easier to determine what our application is doing.

Here is a view of what the services folder looks like.

Alt Text

Looking at the services folder of this simple application gives you a very good idea of what this application does. This can also be very helpful for larger projects.

I hope you found this blog helpful and that you can implement a service object in your next project to keep you controllers and models lean.

Top comments (0)