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