Merdan Durdyyev
Posted on February 8, 2023
Introduction
Hello dear coders, enthusiasts and learners!
Hope you are all doing fine and ready to learn some more tips about Ruby on Rails. This time we are going to mention one of the ways to refactor the code in Ruby on Rails projects.
Actually, code logic that is unrelated models, or controllers can be extracted to separate parts like, Modules, Concerns, Service objects, Helpers and etc. And this time we are going to focus on Service objects.
Do we actually need Rails Service Objects ?
Ruby on Rails is an excellent framework for building MVP or developing large scale applications. As years pass by, the codebase grows into a monstrous giant and you start to understand it’s time to take a different approach.
You must keep unconcerned things separated, everything in its own place, follow DRY concept, skinny models, fat controllers, and etc. to keep things manageable, properly organized and sane.
That’s the next step when you start extracting part of the code into Service Objects, Concerns, extra Modules, or even start migrating to microservices, to prevent the codebase from bloating and exploding one day.
So, the answer is “YES”. We do actually need Service Objects, to offload much of the workload from controllers and other parts of the code.
What are Services Objects in Rails?
When you business logic can’t fit either into models, or controllers, that’s the time you need to go on with Services, to be able to separate unrelated business logic into its own Ruby object.
> Services objects are just Plain Old Ruby Objects (PORO) that are intended to perform a single action at its core.
There’s an opinionated discussion whether where to place your service objects. So, generally, service objects are placed inside either in “lib/services”, or “app/services” folders. You can read couple of articles or discussions on that and come to a decision of your own.
So, a Service Object is a way of encapsulating a part of logic, a single action, or function, into an independent single-use object. It serves a single purpose and exposes only one public method, and is built around that method.
That single public method is generally called “call”, Gitlab calls it “execute” in its services, some like calling it “perform”. But that’s up to you how to call it, as long it is a single public method that will be executed when calling the service.
Creating a Service Object
To create a Service object, we can create a directory for it and create a new file with following command:
$ mkdir app/services && touch app/services/tweet_creator.rb
So, let’s create a new TweetCreator in a new folder called app/services:
# app/services/tweet_creator.rb
class TweetCreator
def initialize(message)
@message = message
end
def send_tweet
client = Twitter::REST::Client.new do |config|
config.consumer_key = ENV['TWITTER_CONSUMER_KEY']
config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET']
config.access_token = ENV['TWITTER_ACCESS_TOKEN']
config.access_token_secret = ENV['TWITTER_ACCESS_SECRET']
end
client.update(@message)
end
end
After you have created the “TweetCreator” Service object, you can call it anywhere in your code, with:
TweetCreator.new(params[:message]).send_tweet
What are examples of using a Service Object?
There can be dozens of situations when we need to extract some part of the code into Service objects. But, just to count some of them, these are the the situations and examples:
- Send a tweet to Twitter — TweetCreator
- Follow a user in Facebook — FacebookUserFollower
- To charge a customer in Ecommerce — CustomerCharger, PaymentCharger
- Send a post to Facebook — FacebookPoster
- Try a payment transaction to payment services (Paypal, Stripe)
These are just some of the widely used scenarios of Service objects. But of course, your project can have something very different compared to these examples. So, analyze it, and if it does not fit into model or controller, move it out to a Service Object.
Rules for writing good Service Objects in Rails
There are many ways you can name your Service Objects. But you need to stick to a single naming convention you chose in you own codebase to be able to easily differentiate Service Objects.
Here are some options for choosing naming conventions:
- UserCreator, PaymentCharger, WelcomeMessenger, AccountValidator
- CreateUser, ChargePayment, MessageUser, ValidateAccount, etc…
Here are general rules for writing meaningful and reasonable Service objects:
- Only one public method per Service object
- Name Service objects like regular roles in a Company (TweetCreator, TweetReader, FacebookPoster, etc.)
- No generic objects to perform several actions
- Handle exceptions inside the Service object.
Benefits of using a Service Object in Rails
Service Objects are a great way to decouple your application logic from your controllers. You can use them to separate concerns and reuse them in different parts of your application. With this pattern, here are the benefits you get:
# Clean controllers
Fat controllers, bloating code within a class, are a sleeping dragon, napping monster, that can wake up one day and cause you tons of trouble. When that happens, you might get lost in the tens or hundreds of lines of code, not understanding what it actually does. So, it is an advantage to have a clean, tidy few lines of code that whispers you what it is intended for while you are reading it.
# Ease of testing
Applying separation of concerns, and extracting the unrelated pieces of code, so that they live on their own, allows easier testing of code and interacting with them independently.
# Reusable service objects
Extracting some code to specific Service Objects, gives you a freedom of calling them wherever you want, in your controllers, background jobs, or other services. Thus, you get a reusable piece of code.
# Separation of concerns
Rails controllers see Service objects and interact with them when required. This provides a way to decouple lots of logic into separate piece of code, especially if you want to migrate to microservices architecture. In that case you can extract and move those services with minimal distraction and modification.
Syntactic Sugar for Service objects
Just think about it, this feels great in theory, but TweetCreator.new(params[:message]).send_tweet
is a lot to write. It is too verbose with lots of redundant words. We need to instantiate it and call a method to send the tweet.
We need to shorten it to a single call, so that single call does it all for us. So, there is Proc.call
that calls it and executes itself immediately with the given parameters.
A proc
can be call
-ed to execute itself with the given parameters. Which means, that if TweetCreator
were a proc, we could call it with TweetCreator.call(message)
and the result would be equivalent to TweetCreator.new(params[:message]).call
We can apply the same for the Service object.
Example #1
First, because we probably want to reuse this behavior across all our service objects, let’s borrow from the Rails Way and create a class called ApplicationService
:
# app/services/application_service.rb
class ApplicationService
def self.call(*args, &block)
new(*args, &block).call
end
end
After that, we can inherit TweetCreator from “ApplicationService”:
# app/services/tweet_creator.rb
class TweetCreator < ApplicationService
attr_reader :message
def initialize(message)
@message = message
end
def call
client = Twitter::REST::Client.new do |config|
config.consumer_key = ENV['TWITTER_CONSUMER_KEY']
config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET']
config.access_token = ENV['TWITTER_ACCESS_TOKEN']
config.access_token_secret = ENV['TWITTER_ACCESS_SECRET']
end
client.update(@message)
end
end
And here is the magic “call”. We can call it in the controller this way:
class TweetController < ApplicationController
def create
TweetCreator.call(params[:message])
end
end
Example #2
Here is one more example of a Service object, that extracts process of updating a trip to a separate Service object.
If we want to reuse this behavior for other service objects, we can add a new class called BaseService
or ApplicationService
and inherit from it for our TripUpdateService
:
# app/services/base_service.rb
class BaseService
def self.call(*args, &block)
new(*args, &block).call
end
end
And here is the “TripUpdateService”:
# app/services/trip_update_service.rb
class TripUpdateService < BaseService
def initialize(trip, params)
@trip = trip
@params = params
end
def call
distance_and_duration = calculate_trip_distance_and_duration
(@params[:start_address],
@params[:destination_address])
@trip.update(@params.merge(distance_and_duration))
end
private
def calculate_trip_distance_and_duration(start_address, destination_address)
distance = Google::Maps.distance(start_address, destination_address)
duration = Google::Maps.duration(start_address, destination_address)
{ distance: distance, duration: duration }
end
end
then we can update our controller action to call the service object correctly:
# app/controllers/trips_controller.rb
class TripsController < ApplicationController
def update
@trip = Trip.find(params[:id])
if TripUpdateService.call(@trip, trip_params)
redirect_to @trip
else
render :edit
end
end
end
Conclusion
Finally we are at the end of the article about Service objects.
As you might have already read, Service objects are really a helping hand to avoid code bloat and cluttering the code logic. But it is up to you whether to use it or not.
If the code fits the criteria when it should be extracted to a Service, then it’s a good opportunity for you to separate concerns and make your code much more readable.
Hope so much, this article helped you learn at least a tiny bit of something new about Ruby on Rails and some programming concepts.
See you in the next article. Stay safe and eager to learn !!!
Posted on February 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.