Quick Summary:
The main principle of software development is to keep the code DRY (Don’t Repeat Yourself) to reduce the reduction in the code. So, Ruby On Rails follows some design patterns to achieve the DRY principle. Though we can use models, helpers, and concerns to make the code dry, if the code complexity is higher or we are using an external API, in that case, we use Design Patterns in Rails.
Table of Contents
Design patterns can be considered best practices that must be followed to avoid code complexity and maintain code readability. Implementing such design patterns also lessens the efforts of fellow developers to understand the business logic.
Design patterns are concepts and best practices for reducing complexity; they are concrete code blocks. The intention behind design patterns is to channel the business logic and algorithm of the problem.
In today’s blog, we will discuss Rails design patterns. You might have heard of design patterns in Rails. Moving on to our first section, let’s understand why we should use design patterns in the first place.
Before learning about Ruby on Rails design patterns and diving deeper into that; let’s go through a few reasons why use design patterns in Rails.
Okay, so this was an overview of Ruby on Rails design patterns. Let’s move towards the next section and see the top 10 design patterns in Ruby on Rails.
We have curated the list of the best crucial design patterns in Rails, which will help you to develop scalable and efficient RoR applications. These following Ruby on Rails design patterns will improve your code quality and accelerate your development process efficiently.
1. Service Objects
2. View Objects (Presenter)
3. Query Objects
4. Decorators
5. Form Objects
6. Value Objects
7. Policy Objects
8. Builder
9. Interactor
10. Observer
Let’s discuss all the Ruby design patterns one by one with examples.
It is PORO – Plain Old Ruby Object, which is meant to encapsulate business logic and complex calculations into manageable classes and methods. This is one of the most used design patterns in Ruby on Rails.
Implementing Design Pattern in your RoR project is not challenging anymore
Get in touch with us to hire Ruby on Rails developer and enhance the performance of your existing RoR application.
In this example, we perform Stripe integration with the help of a Service Object. The Stripe service will create Stripe customers based on their email addresses and tokens.
Look at PaymentsController; here, you can see that the controller is too skinny. The payment service has solved the problem of too much code inside the controller and made the controller skinny and readable.
class PaymentsController < ApplicationController def create PaymentService.new(params).call redirect_to payments_path rescue Stripe: : CardError => e flash[:error] = e.message redirect_to new_Payment_path end end
Now create payment_service.rb inside your_project/app/services folder.
class Payment Service
def initialize(options = {})
options.each pair do Jkey, value
instance_variable_set("@#{key}", value)
end
end
def call
Stripe: : Charge.create(charge_attributes),
end
private
attr_reader : email, :source, :amount, :description
def amount
@amount. to_i * 100
end
def customer
@customer ||= Stripe::Customer.create(customer_attributes),
def customer_attributes
{
email: email,
source: source
}
end
def charge_attributes
{
customer: customer.id,
amount: amount,
description: description
}
end
end
View Objects allow us to encapsulate all view-related logic and keep both models and views neat. They are the kind of design patterns in Rails that are easy to test as they are just classes.
To solve the calculation logic problem, we can use the Rails helper, but if the code is complex, we should use the Presenter.
Have a look at the below code of the view page.
User Full Name: <%= "#{user.first_name} #{user. last_name}" %> <%= link_to "View More Detail", user, class: "W-75 p-3 text-#{user.active? ? "orange" : " green"} border-#{user.active? ? "orange" : "green"}" %>
Here, we can see that we concatenate the user’s first_name and last_name on the view, which is not a good practice. Hence to solve this problem, we should use the presenter.
Let’s create a presenter class to solve this.
class UserPresenter
def initialize(user)
@user = user
end
def full_name
"#{@user.first_name} #{@user. last_name}",
end
def css_color
@user.active?? "orange" : "green",
end
end
Save it under app/presenters/user_presenter.rb and create the presenter’s folder if you do not have it. Now let’s change the view to make it more readable without any calculations.
<% presenter = UserPresenter.new(user) %>
User Full Name: <%= presenter.full_name %>
<%= link_to "View More Detail", user, class: "W-75 p-3 text-#{presenter.css_color} border-#{presenter.css_color}" %>
Query Object is a design pattern in Rails that lets us fetch query logic from Controllers and Models into reusable classes.
Let’s have a look at the below example.
We want to request a list of posts with the type “video” that has a view count greater than 1000 and that the current user can access.
class PostsController < ApplicationController def index @posts = Post.accessible_by(current_ability) .where(type: :video) .where('view_count > ?', 100), end end
The problems in the above code are:
To make the controller skinny, readable, and neat, we can use scopes:
class Post < ActiveRecord::Base,
scope : video_type, ->{ where(type: :video) }
scope : popular, -> { where('view_count > ?', 1000) }
scope : popular_video_type, -> { popular.video_type }
end
So, the controller will look like that:
class Articles Controller < ApplicationController
def index
@posts = Post.accessible_by(current_ability),
.popular_video_type
end
end
But still, it is not an appropriate solution; here we need to create scopes for every query condition we want to add, we are also increasing the code in the Model with various combinations of scope for diverse use cases.
To solve this kind of problem, we use the Query Object:
// video_query.rb
class VideoQuery
def call(ability)
ability
.where(type: : video)
.where('view_count > ?', 1000)
end
end
// post_controller.rb
class PostsController < ApplicationController
def index
ability = Post.accessible_by(current_ability),
@articles = VideoQuery.new.call(ability)
end
end
Now, it’s reusable! We can use this class to query any other models that have a similar scheme.
Another Rails design pattern is the Decorator. The decorator allows behavior to be added dynamically to an object without disturbing the behavior of other objects of the same class. Decorators can help clean up logic/code written inside the view and controller in a RoR application.
module ApplicationHelper
def decorate(model_name, decorator_class = nil),
(decorator_class || "#{model_name. class}Decorator".constantize).new(model_name),
end
end
class BaseDecorator < SimpleDelegaton def decorate (model_name, decorator_class = nil), ApplicationController.helpers.decorate(model_name, decorator_class), end end
class UserDecorator < BaseDecorator
def full name
"#{fname} #{lname}"
end
end
class users Controller < Application Controller
def show
@user_decorator = helpers. decorate(current_user),
end
end
Let’s use this in our view(show.html.erb).
<%= @user_decorator.full_name %> <%= @user_decorator.email %>
The form object is a design patterns in Rails that encapsulates the code related to validation and persistent data into a single unit.
Let’s have a look at the example of the Form Objects. Let’s assume that we have a rails post model and a controller (posts_controller) action for creating the new post. Let’s discuss the problem, Here Post Model contains all validation logic, so it’s not reusable for other entities, e.g., Admin. app/controller/posts_controller.rb
class PostsController < ApplicationController
def create
@post = Post.new(post_params)
if @post. save
render json: @post
else
render json: @post.error, status: :unprocessable_entity
end
end
private
def post_params
params. require(: post).permit(:title, :description, :content)
end
end
// app/model/post.rb
class Post < ActiveRecord: :Base validates :title, presence: true validates : content, presence: true end
The better solution is to move the validation logic to a separate singular responsibility class that we might call PostForm:
class Post Form
include ActiveModel: :Model
include Virtus.model
attribute :id, Integer
attribute :title, String
attribute :description, String
attribute : content, String
attr_reader : record
def persist
@record = id ? Post. find(id) : Post.new
if valid?
@record. save!
true
else
false
end
end
end
Now, We can use it inside our posts_controller like that:
class PostsController < ApplicationController
def create
@form = Post Form.new(post_params)
if @form. persist
render json: @form. record
else
render json: @form.errors, status: :unpocessably_entity
end
end
private
def post_params
params. require(: post).permit(:title, :description, :content)
end
end
The Value object is a type of Ruby pattern that encourages small, simple objects and allows you to compare these objects as per the given logic or specific attributes. It represents value but is not something unique in your system, such as a user object. Value Objects always return only values.
Let’s have a look at the example for better understanding:
class EmailReport
def initialize(emails)
@emails = emails
end
def data
emails_data = [ ]
emails.each do email|
emails_data << {
username: email.match(/([^@]*)/).to_s,
domain: email.split("@"). last
}
end
emails_data
end
private
attr_reader : emails
end
We are doing the following thing with the email:
Now, let’s create the value object:
class Email
def initialize(email)
@email = email
end
def username
email.match(/([^@]*)/).to_s
end
def domain
email.split("a"). last
end
def to_h
{ username: username, domain: domain }
end
private
attr_reader : email
end
Now, we just need to use Email value object link this:
class EmailReport
def initialize(emails: emails),
@emails = emails
end
def data
emails.map { email| Email.new(email).to_h }, end
private
attr_reader : emails
end
The policy object is similar to the Service object in design patterns in Ruby; the only difference is that the policy object is responsible for read operations, while the service object is responsible for write operations. In Rails, we use the cancan or pundit gem for authorization, but these gems are suitable if the application complexity is medium. If the application complexity is high (in terms of authorization), then we use the policy object for better efficiency. It returns a boolean value (true or false).
Let’s look at the example below for a better understanding.
class UserService
def initialize(user)
@user = user
end
def name
user_policy.take_email_as_name?? user.email : user.full_name
end
def account_name
user_policy.is_admin? ? "Administrator": "User",
end
private
attr_reader : user
def user_policy
@_user_policy II = UserPolicy.new(user),
end
end
Let’s create a policy (app/policies/user_policy.rb):
class UserPolicy def initialize(user) @user = user end def is_admin? user_role_is_admin? end def take_email_as_name? user_full_name_is_not_present? && is_user_email_present? end private attr_reader : user def user_full_name_is_not_present? user.full_name.blank? end def is_user_email_present? user.email.present? end def user_role_is_admin? user.sign_in_count > 0 && user.role == "admin" end end
With the help of the Builder pattern, we can construct complex objects without much effort. We can call it an Adapter, whose main purpose is to untangle the complexity of the object instantiation process. Whenever you are dealing with a highly customized product and its complexity the Builder pattern helps to clear the clutter to create the basic concept. Because of it, one can have a fundamental understanding of complex construction.
Our next Rails design pattern is an interactor. Now, the question is, what is the interactor design pattern? So, when you want to disintegrate large and complicated tasks into smaller and inter-dependent steps, you can go with an interactor design pattern. Whenever, a step fails, the flow will stop by itself and will display a relevant message of failed execution.
In the example, we will look at how we can disintegrate the process of purchasing a product from an e-commerce website.
class ManageProducts
include Interactor
def call
# manage products and their details
end
end
class AddProduct
include Interactor
def call
# Add product to purchase
end
end
class OrderProduct
include Interactor
def call
# Order product to purchase
end
end
class DispatchProduct
include Interactor
def call
# dispatch of the product here
end
end
class ScheduleMailToNotify
include Interactor
def call
# send an email to the respective buyer
end
end
class PurchaseProduct
include Interactor::Organizer
organize ManageProducts, AddProduct, OrderProduct, DispatchProduct, ScheduleMailToNotify
end
Here, is how you can purchase the product.
result = PurchaseProdcut.call(
recipient: buyer, product: product
)
puts outcome.success?
puts outcome.message
Moving on to the design pattern in Ruby, i.e., the Observer design pattern. In this pattern, other interested objects are notified whenever an event has occurred. The observed object contains its observers’ list and notifies them by sending an update whenever its state changes.
require 'observer'
class Product
include Observable
attr_reader :productName, :availableProducts
def initialize(productName = "", availableProducts = 0)
@productName, @availableProducts = productName, availableProducts
add_observer(Notifier.new)
end
def update_availableProducts(product)
@product = product
changed
notify_observers(self, product)
end
end
Now, we will build a class which would be notified with the updates on available products.
class Notifier
def update(product, availableProducts)
puts "Yes, #{product.name} is available" if availableProducts > 0
puts "Oops! Sorry, #{product.name} is unavailable!" if availableProducts == 0
end
end
Let’s put them together
isProductAvailable = Product.new("Iphone 12") isProductAvailable.update_availableProducts(5) # => Yes, Iphone 12 is available isProductAvailable.update_availableProducts(8) # => Yes, Iphone 12 is available isProductAvailable.update_availableProducts(0) # => Oops! Sorry, Iphone 12 is unavailable
There are other design patterns in Ruby, but these ten are the fundamentals and most needed Rails design patterns. We hope the blog has served your purpose as you expected. Feel free to use these design patterns in Rails app and see the results by yourself. You can also get in touch with our Ruby on Rails development company for expert guidance and tailored solutions. We will assist you in developing and deploying the best dynamic RoR applications that stand ahead of all.
Your Success Is Guaranteed !
We accelerate the release of digital product and guaranteed their success
We Use Slack, Jira & GitHub for Accurate Deployment and Effective Communication.