10 Must-Know Design Patterns in 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.
1. Service Objects
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.
When to Use Service Objects?
- When we need to perform complex calculations or business logic, e.g., if we need to calculate employees’ salaries based on their attendance.
- When we need to implement any other API, we need to implement a payment gateway such as Stripe.
- When we want to import CSV that contains the bulk of data.
- When we need to clear garbage/unused/old data from the database efficiently, it won’t affect the existing data.
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
2. View Objects (Presenter)
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.
When to Use View Objects?
- When we need to present data in a particular format, like customizing the display of products or user profile
- When we want to simplify complex view logic from the view or controller to a dedicated object.
- When to combine data from different models into a single object for the view, e.g., admin dashboard.
Have a look at the below code of the view page.
<p> 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"}" %> </p>
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}" %>
3. Query Object
Query Object is a design pattern in Rails that lets us fetch query logic from Controllers and Models into reusable classes.
When to Use Query Object?
- When you want to encapsulate complex database queries like fetching records with multiple conditions and advanced filtering logic.
- When you need to keep your ActiveRecord models by driving query logic out of the model and into a dedicated object.
- When you must reuse the same query in different application parts, ensure consistency and eliminate duplication.
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:
- This code isn’t reusable
- It’s hard to test the logic
- Any changes to the post schema can break this logic/code.
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.
4. Decorators
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.
When to Use Decorators?
- When you need to add presentation logic or behavior to an object without modifying its underlying class.
- When to manage multiple representations of the same object, such as specific addresses and user details.
- When you want to follow the Single Responsibility Principle by separating view-specific enhancements from the business logic models.
Process:
- Create an app/decorator folder.
- Add decorate helper in ApplicationHelper.
module ApplicationHelper def decorate(model_name, decorator_class = nil), (decorator_class || "#{model_name. class}Decorator".constantize).new(model_name), end end
- Add base_decorator.rb in app/decorators folder.
class BaseDecorator < SimpleDelegaton def decorate (model_name, decorator_class = nil), ApplicationController.helpers.decorate(model_name, decorator_class), end end
- Add user_decorator.rb in app/decorators folder.
- Initialize @user_decorator in your user_controller.
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).
5. Form Objects
The form object is a design patterns in Rails that encapsulates the code related to validation and persistent data into a single unit.
When to Use Form Objects?
- When you handle complex forms that interact with multiple models
- When you need to customize form submission logic, like performing additional actions after form data without bloating your controllers.
- When you determine how to manage form validation and data processing for non-persistent data, like search and style forms.
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
6. Value Object
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.
When to Use Value Object?
- When you want to represent a simple domain concept that is equally based on its values instead of identity
- When you need to summarize multiple data and behavior into a single object.
- When required assure immutability, making it easier to manage small logic without chances of unintentional side effects.
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:
- We are not changing the email value
- We return only values.
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
7. Policy Object
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).
When to Use Policy Object?
- When you want to keep your app safer by centralizing authorization rules in a single and reusable object.
- When you need to maintain and update permissions easily, ensure that access control changes are handled in one place.
- When testing authorization logic independently of controllers or models, verifying the correctness of permissions is easier.
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
8. Builder
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.
When to Use the Builder pattern?
- When you are creating new objects and you need many permutations of that particular feature
- When you are focused on the process of creating objects and how to assemble them, rather than being dependent on just constructors.
9. Interactor
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.
When to Use the Interactor pattern?
- When you intend to breakdown a large task or process into smaller ones
- When you want to have the flexibility to make changes often in the substeps
- When you want to integrate external APIs into the smaller tasks
Example of Interactor Design Pattern
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
10. Observer
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.
When to Use the Observer pattern?
- When you want to make several views changes manually
- When an object’s state is dependent on a specific state
- When several views are dependent on a particular object’s state
Example of Observer Design Pattern
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