For Developers

Refactoring Your Rails App With Rails Service Objects

Rails App Restructuring With Rails Service Objects

Rails controller applications tend to start simple, with clean controllers and models. After that, you can start adding features. Before you know it, your controllers and models are wide, large, and hard to comply with. When you refactor into rails service objects, you should split these big pieces into something easier to understand, test, and maintain.

Rails service object.webp

A rails service object is a Ruby rails service object which performs a single action as it encapsulates a process in your domain. You can now create a book instance in a library application in a plan rails controller application with this code.


class BookController <ApplicationController>
	def create
		Book.new (*args)
	end
end

Code source

But, when you end with a lot surrounding it, then use the below code:


class BookController <ApplicationController>
def create
	default_args = { genre: find_genre(), author: find_author() }
	Book.new(attrs.merge (default_args))
end

private

def find_genre
// . . . . 
end

def find_author
// . . . .
end
end

Code source

Rails services will let you abstract this behavior into different classes. Then, your code will become simple and easy to understand.


class BookController <ApplicationController>

           def
		BookCreator.create_book

           end
end


Code source

Why do you require rails service objects?

Use of Rails Service Object.webp

The design of the rails service object supports the Model View Controller’s organizational structure. This structure is adequate when you are looking for smaller or simpler applications. But, as your application develops into a complex one, you may begin to see business or domain logic across the controller and the models. Such logic doesn’t belong to the model or controller, so they make the code more difficult to maintain or reuse.

A rails service object is a pattern that will help in separating business logic from models and controllers. It does this by enabling the models to be simple data layers and the controller entry point to your API. We introduce services for encapsulating the basic logic for getting more benefits like:

1. Testable controllers

As the controllers are lean and serve as a collaborator to the service, it becomes easy for testing. This is because we only check whether certain methods within the controller are called when a certain action happens.

2. Lean rails controllers

These controllers are only responsible for understanding requests and turning the sessions, params, and cookies into arguments that will be passed to the service object for action. The controller will then redirect or render as per the service response. Even when you look into larger applications, the controller actions using rails services have more lines of code.

3. Separation of domain & framework

Rails controllers will see services and interact with the domain object using it. This decrease in coupling will make scalability easier, mainly when you want to move from a larger service to a microservice. Your services will be easily extracted and moved to a new one with minimal modification.

4. Reusable services

Rails service objects can also be called controllers, other queued jobs, and service objects. The rails controller is the brain of the application. It will help you with the interaction between users, the models, and the views. It is a home for many vital ancillary services. It is responsible for routing external requests into internal objects.

5. Ability to test business processes in isolation

Services are easier and faster to test as they are small Ruby objects that are separated from their environment. We can easily stub all the collaborators and check whether all the steps are performed during the service.

Creation of a service object

a. Firstly, we create a new BookCreator in a folder under services/apps for a library management application using the code below:


$ mkdir app/services && touch app/services/book_creator.rb

b. Secondly, we dump all our logic inside a new Ruby class using this code:


# app/services/book_creator.rb
class BookCreator
	def initialize (titile:, description:, author_id:, genre_id:)
		@tittle = title
		@description = description
		@author_id = author_id
		@genre_id = genre_id 
	end

	def create_book
		Book.create! 
			(
			title: @title 
			description: @description 
			author_id: @author_id  
			genre_id: @genre_id 
			)
		rescue ActiveRecord: : RecordNotUnique => e
		# handling duplicate entry
		end
	end
end

Code source

c. Thirdly, we call the service object in the controller or anywhere inside the application using this code:


class BookController < ApplicationController
	def create
		BookCreator.new (title: params [:title], description: params [:description],
 author_id: params [:author_id], genre_id: params [:genre_id]).create_book
	end
end

Code source

Service object syntacti sugar

We can add a class method that instantiates the BookCreator and calls the create method for simplifying the BookCreator.new (arguments).create chain using the below code:


# app/services/book_creator.rb
class BookCreator
	def initialize (titile:, description:, author_id:, genre_id:)
		@title = title 
		@description = description 
		@author_id = author_id
		@genre_id = genre_id 
	end

	def call (*args)
		new (*args).create_book
	end

	private

	def create_book
		Book.create! 
		(
			title: @title 
			description: @description 
			author_id: @author_id 
			genre_id: @genre_id 
		)
		rescue ActivateRecord: : RecordNotUnique => e
		# handling duplicate entry
		end
	end
end

Code source

In the controller, you can call the book creator using the following code:


class BookController < ApplicationController
	def create
		BookCreator.call
		(
		title: params [:title],
		description: params [:description],
		author_id: params [:author_id],
		genre_id: params [:genre_id] 
		)
	end
end 

Code source

We can abstract the call method into the base ApplicationService class that each service object will inherit from. We should also make sure that our code doesn’t repeat itself and reuse this behavious with other related rails service objects.


class ApplicationService
	self.call(*args)
		new (*args).call
	end 
end

Code source

We can refactor the BookCreator for inheriting from the ApplicationService using the below code:


# app/services/book_creator.rb
class BookCreator <ApplicationService>
	def initialize (title:, description:, author_id:, genre_id:)
		@title = title
		@description = description
		@author_id = author_id
		@genre_id = genre_id 
	end

	def call
		create_book
	end
	
	private

	def create_book
		#.. . . 
	end 
end 

Code source

Creation of rails service objects using the BusinessProcess gem

You don’t have to create a base application service class or define the initialize method with the BusinessProcess gem. It is because the gem has all the in-built configurations. Your service object will inherit from the BusinessProcess::Base.

In your BusinessProcess gem file, you can do the following:

gem ‘business_process’

Then, you have to run a bundle command in your terminal with the below code:


class BookCreator <BusinessProcess::Base>
	# Specify the requirements
	needs: title
	needs: description
	needs: author_id
	needs: genre_id
	
	# Specify the process
	def call 
		create_book
	end
	private
	
	def create_book
		#. . . . 
	end
end 

Code source

Guidelines to create good rails service objects

Creating Rails Service Objects.webp

Here are the guidelines for creating good rails service objects.

- Name rails service objects as per the role they perform

The name of a service object must indicate what it does. There is a way of naming rails service objects with words ending with ‘er’ and ‘or’. For example, when the job of the service object is to create a book, the name used can be BookCreator, and when the job is reading a book, the name can be BookReader.

- One public method

A service object must perform one business action and make it well, so it will only expose a single public method for the same. Other methods must be private and known by the public method. You must choose to make the public method as you want, provided that the naming is constant across all rails service objects.

- Group rails service objects in namespaces

When you are working on a large application and you introduce a service object, it means you can grow from one service object to more. For improving code organization, it is a great practice for grouping common rails service objects into namespaces. When you consider the library application, we should group all book-related services and author-related services in a separate namespace.

Our rails service objects must be like below:


# services/book/book_creator.rb
module Book
	class BookCreator <ApplicationService>
	. . . . 
	end 
end 

# services/twitter_manager/book_reader.rb
module Book
	class BookReader <ApplicationService>
	. . . . 
	end 
end 


Code source

Our calls will now become Book::BookCreator.call(*args) and Book::BookReader.call(*args).

- Don’t instantiate rails service objects directly

When you use the syntactic sugar pattern or BusinessProcess gems for shortening the notation of calling the rails service objects, it would allow you to simplify BookCreator.new(*args).call or BookCreator.new.call(*args) into BookCreator.call(*args) that is shorter and readable one.

- Rescue exceptions and raise custom exceptions

The service object’s purpose is to encapsulate implementation details inside it, as per the interactions between third-party services or database manipulation, or libraries with Rails ActiveRecord. When an error arises while interacting with an ActiveRecord, the service should rescue the exception properly. You must not allow errors to propagate up the call stack. When it can’t be handled within the rescue block, you should raise a custom-defined exception that is specific to that service object.

- One responsibility per service object

When you have a service object which does more than one thing then it goes against the business action mindset of rails service objects. But, when you have a generic service object, it will perform multiple actions and will be discouraged. Therefore, when you want to share the code among rails service objects, you must create a base or helper module and use the mixins for including your service object.

The service object pattern will improve your application’s overall design as you can add new features to your application. It will make your codebase more expressive and easy to maintain and will enable hassle-free testing.

FAQs:

1. What environments do Rails have by default?

Rails have the test, production, and development environments by default during configuration. There is nothing special about these environments, but there are a few places in the Rails source code that will have reference to them. It is unique because of the way they are configured in an application’s configuration files.

2. Is Ruby on Rails a framework?

Ruby on Rails is a server-side web application framework. It is written in Ruby programming language under the MIT License. Rails is an MVC framework that will provide a default structure for a web page, database, and web service.

3. What is the difference between a class and a module Ruby?

Modules are the collection of constants and methods. Modules cannot generate instances. Modules can be mixed into classes and other similar modules. The mixed-in modules will let constants and methods blend into that class, by augmenting its functionality. A module cannot be inherited from anywhere.

Classes can generate instances and they have a per-instance state. Classes cannot get mixed into anything. A class might inherit from another class, but cannot inherit from a module.

4. What are hashes in Ruby?

In Ruby, hashes are a collection of unique key-value pairs. A Hash can be an array. But the indexing is done with the help of arbitrary keys of any object type. The ordering of returning keys and their value by different iterations is arbitrary and will not generate the insertion order.

Press

Press

What's up with Turing? Get the latest news about us here.
Blog

Blog

Know more about remote work.
Checkout our blog here.
Contact

Contact

Have any questions?
We'd love to hear from you.

Hire and manage remote developers

Tell us the skills you need and we'll find the best developer for you in days, not weeks.

Hire Developers