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
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
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.
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.
- Service Object
- View Object (Presenter)
- Value object
- Builder
- Decorators
- Form Object
- Policy object
- Query object
- Observer
- 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.
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.
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.
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.
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!
#1 Solution for Logs, Traces & Metrics
APM
Kubernetes
Logs
Synthetics
RUM
Serverless
Security
More