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.
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
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.
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
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.
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
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
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
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
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
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
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.
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.