Ruby on Rails: 10 Trending Design Patterns

Developers can develop agile applications using these conventions and write less code. Moreover, Ruby on Rails developers' communities can be enhanced in terms of maintainability and understandability.

Furthermore, the developers use the Rails conventions and sensitive defaults in their web applications, making them more scalable.

Among them are email management, object-database mappings, file structures, code generation, element naming and organization, etc.

A design pattern is a recurring way of solving a problem in software design. Especially when working on large legacy applications without good software design principles, Ruby on Rails applications often encounter such issues.

Table Of Contents

  1. Ruby on Rails
  2. Design Patterns: What are they?
  3. Aspects of employing design patterns

Ruby on Rails

All applications in Rails are written in the Ruby programming language, which makes Ruby an open-source web framework. As an agile web development framework, Ruby on Rails focuses on productivity.

The Ruby on Rails framework is a web application development framework that allows you to create data-driven applications. As a result of its productivity and agility features, this framework is becoming increasingly popular among agile developers.click

Ruby on Rails
Ruby on Rails

A number of common problems developers encounter while developing web applications can be solved with Ruby on Rails. In addition, this framework uses convention rather than configuration, which results in a more agile development process.

Design Patterns: What are they?

As a solution to a typical problem, a pattern can be reused in software engineering. In the world of software engineering, the pattern is considered a good practice. In software engineering, patterns can quickly become their contrary anti-patterns, but more on that later.

A design pattern can be thought of as a set of best practices for avoiding code complexity and maintaining code readability. By implementing such design patterns, fellow developers are also less likely to struggle to understand the business logic.

Design patterns reduce complexity by combining concepts and best practices, they are concrete blocks of code. Design patterns serve as channels for business logic and algorithms.

Aspects of employing design patterns

By utilizing design patterns appropriately, we can benefit our architecture in the following ways:

  • A faster software development process - Patterns that are well-designed and tested will speed up the process of creating software.
  • Providing bug-free solutions - In addition to reducing the number of issues that are not visible at an early stage of development, design patterns also help in preventing issues in the future that may become more apparent.
    In the absence of design patterns, extending the code or handling more scenarios will be more challenging.
  • Self-documenting and readable code - We can improve the readability of our code by applying specific architecture rules. Other developers who are not involved in the creation process will be able to follow the rules more easily.
Aspects of employing design patterns
Aspects of employing design patterns

This blog is about Rails design patterns. A design pattern might be familiar to you from Ruby on Rails. Now let's take a look at the top ten rails design patterns.

  1. Service Object
  2. View Object (Presenter)
  3. Value object
  4. Builder
  5. Decorators
  6. Form Object
  7. Policy object
  8. Query object
  9. Observer
  10. Interactor

1. Service Object

In Ruby, PORO - Plain Old Ruby Object  is used for encapsulating complex calculations and business logic into manageable classes and methods. Ruby on rails uses service object design pattern quite often.

service object

Employing service object

  • For instance, when calculating employees' salaries based on their attendance requires complex calculations or business logic.
  • In order to implement any other API, a payment gateway like Stripe must be implemented.
  • When we want to import CSV that contains the bulk of data. 4. Data can be efficiently cleared from the database once garbage, unused, or old data has been removed.

One of the basic concepts of a service object is that it performs no more than one function:

class WebsiteTitleScraper
	def self.call(url)
		response = RestClient.get(url)
		Nokogiri::HTML(response.body).at('title').text
	end
end

Scraping the title of a website is all that the above class is responsible for.

2. View Object (Presenter)

By using view object, we are able to encapsulate all logic related to views and keep both models and views organized. Rails patterns such as view object are easily tested since they are just classes.

view object
view object 

Using rails helper, we can solve the calculation logic problem, however, if the code is complex, the Presenter should be used instead.

The purpose of this design pattern is to isolate the more advanced logic used within Rails' views:

class UserPresenter
	def initialize(user)
		@user = user
	end

	def status
		@user.sign_in_count.positive? ? 'active' : 'inactive'
	end
end

It is advisable to keep the views as simple as possible without including any business logic. A presenter is a good solution for code isolation that makes testing and interpreting the code easier.

3. Value Object

Value object are types of Ruby patterns that encourage small, simple objects and allow comparison based on given logic or specific attributes.

A value object does not represent something specific to your system like a user object. There is only one value returned by a Value Object.

By using the value object pattern, we will create a simple and plain Ruby class that will only return values in the form of methods:

class Email
	def initialize(value)
		@value = value
	end

	def domain
		@value.split('@').last
	end

In the above class, the value of the email is parsed and the data pertaining to it is returned.

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

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

In addition to being called a builder pattern, an adapter pattern is often used as well. As a result of using this pattern, we can easily return a specific class or instance depending on the case. The following builder can be created if you are parsing files for content:

class FileParser
	def self.build(file_path)
		case File.extname(file_path)
		when '.csv' then CsvFileParser.new(file_path)
		when '.xls' then XlsFileParser.new(file_path)
		else
			raise(UnknownFileFormat)
		end
	end
end

class BaseParser
	def initialize(file_path)
		@file_path = file_path
	end
end

class CsvFileParser < BaseParser
	def rows
		# parse rows
	end
end

class XlsFileParser < BaseParser
	def rows
		# parse rows
	end

You can access the rows if you have the file_path without worrying about selecting a class capable of parsing the given format:

parser = FileParser.build(file_path)
rows = parser.rows

5. Decorators

Rails also offer a Decorator as a design pattern. Decorators provide a way to dynamically add behavior to objects without interfering with the behavior of other objects in the same class.

Decorator
Decorator 

When developing an application using RoR, decorators can be useful for cleaning up logic/code within views and controllers.

Using this process:

  • Set up an app/decorator folder.
  • In ApplicationHelper, add a decorate helper.

6. Form Object

A form object encapsulates the code related to data validation and persistence into a single unit. Thus, the form unit can be reused throughout the codebase and the logic can be detached from the views.

Employing form object

A design pattern is both a means and a tool for creating more efficient and robust applications. Relying on how developers determine them, these patterns can be applied to a vast array of contexts.

We have therefore outlined a short checklist of criteria for identifying when Form Objects are appropriate:

  • Resources of more than one kind are affected - In most cases, this condition is met first. Traditionally, Rails controllers and models are built on single resources. A good indication of the need to use Form Objects is when the business logic requires initializing and manipulating multiple resources. The term "affected" refers to creations, updates, destructions, or a combination of creations, updates, and deletions occurring simultaneously.
  • A lot of just custom logic validation is required - In most cases, resources such as models have embedded validations, such as making sure some attributes are not nil or must adhere to specific conditions (greater than 0, a specific pattern, or a pattern).
    Active Record's validate method calls are referred to as:
class User < ApplicationRecord
	validates :username, presence: true, uniqueness: true
	validates :locale, 
	inclusion: { in: Language.all.map(&:code).map(&:to_s) }
	validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }
	validates :terms_of_service, :informed_consent, acceptance: true


So, every software application requires validation. Is it possible to validate multiple models at the same time? Does additional validation need to be performed only in certain contexts?

Does it matter if it's included in models even though it's not always required? Is it possible to write tests for all edge cases efficiently and cover all edge cases? Form objects are also needed if these questions arise.

The same business logic needs to be re-used in many places - Users often have the option of performing the same action in different contexts when running applications. Depending on the eCommerce platform, users may be able to make a payment during checkout, along with additional order addendums.

It is essential to create an order and persist it, as well as process a financial transaction in both cases. There is often an unknown error associated with both processes.

So centralization of business logic is paramount when it comes to critical flows. A checkout process should behave similarly across areas of the application, whether the application owner or the end user wants that to happen.

Therefore, Form Objects are an excellent place to host business logic when the same action can be performed in different contexts.

7. Policy object

Service and policy object have the same properties. The only difference is the responsibility for read operations falls on the policy object and the responsibility for write operations falls on the service object.

To authorize an application using rails, we use the cancan or pundit gems, but they are best used if the complexity is medium. If the complexity is high (in terms of authorization), then we use policy objects to better perform the task. Upon completion, a boolean value is returned (true or false).

Consider the subsequent instance in order to gain a deeper 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

Here's how we 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. Query object

By using Query Objects, we can separate query logic from Controllers and Models and make them reusable.

Query object classes isolate the logic for querying databases as their term signifies. If we want to keep simpler queries in the model, we can keep them inside the model, but if we want to keep more complex queries inside a separate class, we can do so as outlined below:

class UsersListQuery
	def self.inactive
		User.where(sign_in_count: 0, verified: false)
	end

	def self.active
		User.where(verified: true).where('users.sign_in_count > 0')
	end

	def self.most_active
		# some more complex query
	end
end

There is no necessity for the query object to enforce only class methods; it can also equip instance methods that can be stacked when required.

9. Observer

Finally, we will look at the Observer design pattern in Ruby. This pattern sends notifications whenever an interesting event occurs to other interesting objects. Whenever an observer's state changes, an update is sent to the observers of the observed object.

Employing Observer pattern

  • If you need to manually make several changes to a view
  • Objects whose states are determined by a specific state
  • When several views depend on the state of a particular object

Instance 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

10. Interactor

Rails' next design pattern is the interactor. In order to answer this question, we have to define interactor design patterns.

In this case, an interactor design pattern will make it easier for you to break down large, complicated tasks into smaller, interdependent steps. The flow will automatically stop whenever a step fails, and an error message will be displayed.

Employing Interactor pattern

  • If you are planning to break down a large process or task into smaller steps
  • Frequently changing the sub steps should be possible when you want to have the flexibility to do so
  • In the case of smaller tasks that require the integration of external APIs

Instance of Interactor Design Pattern

In this instance, we will look at how can we 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

Final thoughts

Although there are other Ruby on Rails design patterns, these ten are the most essential and fundamental. To build web applications, we can stand on the shoulders of giants like Rails.

This brings significant benefits like speed, security, maintainability, and resilience. Nevertheless, the framework is only the base - the strong and giant piece - on which the application rests.

In order to improve the engineering of the application, developers can append to it and use it to extend the features. Ruby on Rails’ default toolkit includes Form Objects, which are a great addition.

By isolating business logic from the framework, decomposing the implementation and simplifying testing, they allow for better business logic separation. Application maintenance is easier and applications are more resilient to changes.


Ruby Performance Monitoring for Atatus

The Ruby performance monitoring tool provides end-to-end visibility. Identify performance bottlenecks for your application with Ruby monitoring.

With Atatus, identify which code blocks are slow, what database queries are involved, what external services are involved, which templates are used, what message queues are involved and much more. In context with the original request, view logs, infrastructure metrics, and VM metrics.

Get an overview of the end-to-end business transactions in your Ruby application automatically. Ruby Monitoring tracks HTTP status code failures and application crashes. Perform a response time analysis to identify Ruby performance issues and errors. Know how methods and database calls affect your customers.

Ruby performance monitoring

Check how your Ruby server uses SQL and NoSQL queries. Utilize database monitoring proactively to identify and optimize slow database queries. Monitoring and measuring third-party API call response times and REST API failure rates along with HTTP status codes.

Using error tracking, every Ruby error is captured with a complete stack trace, with the exact line of source code highlighted for easier bug fixing. Get all the messages, URLs, request agents, versions, and other essential data to fix Ruby errors and exceptions.

Get your app running faster and bug-free with Atatus, get started with a 14-day free trial!

Aarthi

Aarthi

Content Writer at Atatus.
Chennai

Monitor your entire software stack

Gain end-to-end visibility of every business transaction and see how each layer of your software stack affects your customer experience.