DEV Community

Davide Santangelo
Davide Santangelo

Posted on

Rails Service Objects: A Tiny Guide

Service objects are an important concept in the Ruby on Rails framework, and they are a crucial component of the Model-View-Controller (MVC) architecture. Service objects are classes that encapsulate business logic, and they are responsible for performing a specific set of tasks within a Rails application. They can be used to perform tasks such as interacting with APIs, performing calculations, and performing database operations.

The concept of a service object is not unique to Ruby on Rails, but it has become increasingly popular in recent years due to the growing complexity of web applications. In this article, we will explore the concept of service objects in Rails and how they can be used to improve the structure and organization of Rails applications.

What is a Service Object?

A service object is a Ruby class that encapsulates a specific set of functionality within a Rails application. It is responsible for performing a single, well-defined task that is often not directly related to any specific model or controller within the application. Service objects are designed to be reusable and modular, which makes them easy to test and maintain.

The primary purpose of a service object is to isolate and encapsulate business logic that does not fit neatly into a model or controller. This could be a calculation that involves multiple models or a task that involves external APIs or services. By isolating this functionality in a service object, you can improve the overall structure and organization of your application, and make it easier to maintain and test.

Service objects can be used in a variety of contexts within a Rails application. For example, you might use a service object to process payments, send emails, or interact with an external API. In each case, the service object would encapsulate the necessary functionality and provide a clean, well-defined interface for other parts of the application to use.

Implementing Service Objects in Rails

Implementing a service object in Rails is relatively straightforward. You can create a new class in the app/services directory, and then define the necessary methods and functionality within that class. Here is an example of a simple service object that calculates the total price of a shopping cart:

class CalculateTotalPrice
  def initialize(cart)
    @cart = cart
  end

  def call
    @cart.items.sum(&:price)
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, the CalculateTotalPrice class takes a shopping cart as an argument and calculates the total price of all items in the cart. The call method is the entry point for the service object, and it returns the calculated total price.

To use this service object within a controller, you would simply create a new instance of the CalculateTotalPrice class and call the call method:

class CartsController < ApplicationController
  def show
    @cart = Cart.find(params[:id])
    @total_price = CalculateTotalPrice.new(@cart).call
  end
end
Enter fullscreen mode Exit fullscreen mode

n this example, the show method of the CartsController creates a new instance of the CalculateTotalPrice class and passes the shopping cart as an argument. The call method is then called, and the calculated total price is assigned to the @total_price variable.

Benefits of Using Service Objects

There are several benefits to using service objects within a Rails application. One of the primary benefits is improved organization and structure. By isolating business logic in service objects, you can make your code more modular and easier to maintain. Service objects can also help to reduce the complexity of models and controllers, which can make it easier to understand and reason about the application as a whole.

Another benefit of using service objects is improved testability. Because service objects encapsulate well-defined functionality, they can be tested in isolation using a variety of testing frameworks.

This can help to further demonstrate how service objects can improve testability in Rails applications, let's consider a more complex example.

Imagine you have an e-commerce application that includes a feature for calculating shipping costs. The shipping cost calculation involves multiple factors, including the weight of the items, the destination address, and the shipping method selected by the user.

To implement this functionality in a Rails application, you might start by adding a method to the Cart model to calculate the shipping cost:

class Cart < ApplicationRecord
  def shipping_cost(destination_address, shipping_method)
    # calculate shipping cost based on destination_address and shipping_method
  end
end
Enter fullscreen mode Exit fullscreen mode

This approach works, but it has a few drawbacks. First, the logic for calculating shipping costs is mixed in with the Cart model, which can make it harder to understand and maintain. Second, it can be difficult to test the shipping cost calculation in isolation, since it is tightly coupled to the Cart model.

To address these issues, you could refactor the shipping cost calculation into a service object. Here's an example implementation:

class CalculateShippingCost
  def initialize(cart, destination_address, shipping_method)
    @cart = cart
    @destination_address = destination_address
    @shipping_method = shipping_method
  end

  def call
    # calculate shipping cost based on @cart, @destination_address, and @shipping_method
  end
end
Enter fullscreen mode Exit fullscreen mode

In this implementation, the CalculateShippingCost service object takes the Cart object, the destination address, and the shipping method as arguments. The call method performs the shipping cost calculation.

Now, in your controller, you can use the service object like this:

class OrdersController < ApplicationController
  def create
    @cart = Cart.find(params[:cart_id])
    @shipping_cost = CalculateShippingCost.new(@cart, params[:destination_address], params[:shipping_method]).call
    # create order and process payment
  end
end
Enter fullscreen mode Exit fullscreen mode

By using a service object, you've improved the organization and testability of your code. You can easily test the shipping cost calculation in isolation using a testing framework like RSpec or MiniTest. This makes it easier to catch bugs and make changes to the shipping cost calculation in the future.

Conclusion

Service objects are a powerful tool for improving the structure, organisation and testability of Rails applications. By encapsulating business logic in service objects, you can make your code more modular and easier to maintain. Service objects also make it easier to test complex functionality in isolation, which can help you find bugs and make changes with confidence.

When implementing service objects in your Rails application, remember to focus them on a single task and use a descriptive name that accurately reflects their purpose. This will make your code easier to read and understand, and will help ensure that your service objects remain modular and reusable.

Top comments (6)

Collapse
 
yet_anotherdev profile image
Lucas Barret

I came from a Java SpringBoot, and it is a common pattern to use.
Now I use Rails and I really love it. Nonetheless I was quite surprised by the difference in the pattern that we use.
At the end of the day, it is really cool to improve your skills by taking what fit you in different language/framework/tech.

Collapse
 
superails profile image
Yaroslav Shmarov

absolutely agree with all the benefits you listed. service objects are always present in any ruby app I've seen.

Collapse
 
abitrolly profile image
Anatoli Babenia

It is still not convincing for me that this is better

module Library
  class CalculateShippingCost
    def initialize(cart, destination_address, shipping_method)
      @cart = cart
      @destination_address = destination_address
      @shipping_method = shipping_method
     end

    def call
        # calculate shipping cost based on @cart, @destination_address
        # and @shipping_method
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

than that

module Library
  class ShippingCost
    def calculate(cart, destination_address, shipping_method)
      # do the stuff
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
Collapse
 
moltenhead profile image
Charlie Gardai • Edited

Something I use frequently : turn your method call static =>

module Services
  class Base
    def self.call(*args)
      new(*args).call
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Then you can extend your Services :

module Services
  class Foo < Base
  # ...
Enter fullscreen mode Exit fullscreen mode

Then simply do :

Services::Foo.call(*args)
Enter fullscreen mode Exit fullscreen mode

Also you can use a payload system or railway pattern to ensure your behaviours. And don't forget unit tests <3

Collapse
 
sergiomaia profile image
sergiomaia • Edited

Service Objects are cool, but I prefer writing modular, expressive and sequentially logical ruby code represented by use cases. With a gem like github.com/serradura/u-case we can code in a simple way (input >> process >> output) . Easy to test, easy to maintain and easy to understood. In a big app, Service Objects tends to be a transfer from fat models to fat services.

Collapse
 
hoangvn2404 profile image
Lee Nguyen

thank for posting about u-case, it look amazing to me