Rails design patterns
A design pattern is a repeatable solution to solve common problems in a software design. When building apps with the Ruby on Rails framework, you will often face such issues, especially when working on big legacy applications where the architecture does not follow good software design principles.
This article is a high-level overview of design patterns that are commonly used in Ruby on Rails applications. I will also mention the advantages and disadvantages of using design patterns as, in some cases, we can harm the architecture instead of making it better.
Advantages of using design patterns
An appropriate approach to using design patterns brings a lot of essential benefits to the architecture that we are building, including:
- Faster development process - we can speed up software creation by using tested and well-designed patterns.
- Bug-free solutions - by using design patterns, we can eliminate some issues that are not visible at an early stage of the development but can become more visible in the future. Without design patterns, it can become more challenging to extend the code or handle more scenarios.
- More readable and self-documentable code - by applying specific architecture rules, we can make our code more readable. It will be easier to follow the rules by other developers not involved in the creation process.
Disadvantages of using design patterns in a wrong way
Although design patterns were created to simplify and improve the architecture development process, not appropriate usage can harm the architecture and make the process of extending code even harder.
The wrong usage of design patterns can lead to:
- The unneeded layer of logic - we can make the code itself more simple but split it into multiple files and create an additional layer of logic that will make it more challenging to maintain the architecture and understand the rules by someone who is not involved in the creation process since day one.
- Overcomplicated things - sometimes a meaningful comment inside the class is enough, and there is no need to apply any design patterns which only seemingly clarify the logic.
Commonly used design patterns in Rails applications
This section of the article covers the most popular design patterns used in Ruby on Rails applications, along with some short usage examples to give you a high-level overview of each pattern’s architecture.
Service
The service object is a very simple concept of a class that is responsible for doing only one thing:
class WebsiteTitleScraper
def self.call(url)
response = RestClient.get(url)
Nokogiri::HTML(response.body).at('title').text
end
end
The above class is responsible only for scraping the website title.
Value object
The main idea behind the value object pattern is to create a simple and plain Ruby class that will be responsible for providing methods that return only values:
class Email
def initialize(value)
@value = value
end
def domain
@value.split('@').last
end
end
The above class is responsible for parsing the email’s value and returning the data related to it.
Presenter
This design pattern is responsible for isolating more advanced logic that is used inside the Rails’ views:
class UserPresenter
def initialize(user)
@user = user
end
def status
@user.sign_in_count.positive? ? 'active' : 'inactive'
end
end
We should keep the views as simple as possible and avoid putting the business logic inside of them. Presenters are a good solution for code isolation that makes the code more testable and readable.
Decorator
The decorator pattern is similar to the presenter pattern, but instead of adding additional logic, it alters the original class without affecting the original class’s behavior.
We have the Post
model that provides a content attribute that contains the post’s content. On the single post page, we would like to render the full content, but on the list, we would like to render just a few words of it:
class PostListDecorator < SimpleDelegator
def content
model.content.truncate(50)
end
def self.decorate(posts)
posts.map { |post| new(post) }
end
private
def model
__getobj__
end
end
@posts = Post.all
@posts = PostListDecorator.decorate(@posts)
In the above example, I used the SimpleDelegator
class provided by Ruby by default, but you can also use a gem like Draper that offers additional features.
Builder
The builder pattern is often also called an adapter. The pattern’s main purpose is to provide a simple way of returning a given class or instance depending on the case. If you are parsing files to get their contents you can create the following builder:
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
end
Now, if you have the file_path
, you can access the rows without worrying about selecting a good class that will be able to parse the given format:
parser = FileParser.build(file_path)
rows = parser.rows
Form object
The form object pattern was created to make the ActiveRecord’s models thinner. We can often create a given record in multiple places, and each place has its rules regarding the validation rules, etc.
Let’s assume that we have the User
model that consists of the following fields: first_name
, last_name
, email
, and password
. When we are creating the user, we would like to validate the presence of all attributes, but when the user wants to sign in, we would like only to validate the presence of email and password:
module Users
class SignInForm
include ActiveModel::Model
attr_accessor :email, :password
validates_presence_of :email, :password
end
end
module Users
class SignUpForm
include ActiveModel::Model
attr_accessor :email, :password, :first_name, :last_name
validates_presence_of :email, :password, :first_name, :last_name
def save
return false unless valid?
# save user
end
end
end
# Sign in
user = Users::SignInForm.new(user_params)
sign_in(user) if user.valid?
# Sign up
user = Users::SignUpForm.new(user_params)
user.save
Thanks to this pattern, we can keep the User
model as simple as possible and put only the logic shared across all places in the application.
Policy object
The policy object pattern is useful when you have to check multiple conditions before performing a given action. Let’s assume that we have a bank application, and we would like to check if the user can transfer a given amount of money:
class BankTransferPolicy
def self.allowed?(user, recipient, amount)
user.account_balance >= amount &&
user.transfers_enabled &&
user != recipient &&
amount.positive?
end
end
The validation logic is isolated, so the developer who wants to check if the user can perform the bank transfer doesn’t have to know all conditions that have to be met.
Query object
As the name suggests, the class following the query object pattern isolates the logic for querying the database. We can keep the simple queries inside the model, but we can put more complex queries or group of similar queries inside one separated class:
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
Of course, the query object doesn’t have to implement only class methods; it can also provide instance methods that can be chained when needed.
Observer
The observer pattern was supported by Rails out of the box before version 4, and now it’s available as a gem. It allows performing a given action each time an event is called on a model. If you would like to log information each time the new user is created, you can write the following code:
class UserObserver < ActiveRecord::Observer
def after_create(user)
UserLogger.log("created user with email #{user.email}")
end
end
It is crucial to disable observers when running tests unless you test the observers’ behavior as you can slow down all tests.
Interactor
The interactor pattern is all about interactions. Interaction is a set of actions performed one by one. When one of the actions is stopped, then other actions should not be performed. Interactions are similar to transactions, as the rollback of previous actions is also possible.
To implement the interactor pattern in the Rails application, you can use a great interactor gem. If you are implementing the process of making a bank transfer, you can create the following structure:
class VerifyAccountBalance
include Interactor
def call
return if context.user.enabled? && context.account.balance >= context.amount
context.fail!(message: 'not enough money')
end
end
class VerifyRecipient
include Interactor
def call
return if context.recipient.enabled? && some_other_procedure
context.fail!(message: 'recipient is invalid')
end
end
class SendMoney
include Interactor
def call
# perform the bank transfer
end
end
Each class represents one interaction and can now be grouped:
class MakeTheBankTransfer
include Interactor::Organizer
organize VerifyAccountBalance, VerifyRecipient, SendMoney
end
We can now perform the whole interaction by calling the organizer along with the context data. When one of the interactors fail, the next interactors won’t be executed, and you will receive a meaningful output:
outcome = MakeTheBankTransfer.call(
user: user, amount: 100.0, recipient: other_user, account: account
)
outcome.success? # => false
outcome.message # => "recipient is invalid"
The interactor pattern is a perfect solution for complex procedures where you would like to have full control over the steps and receive meaningful feedback when one of the procedures fail to execute.
Null object
The null object pattern is as simple as the value object as they are based on plain Ruby objects. The idea behind this pattern is to provide a value for non-existing records.
If in your application the user can set its location, and you want to display information when it’s not set, you can achieve it by using the if
condition or creating the NullLocation
object:
class NullLocation
def full
"not set yet"
end
end
Inside the User model, you can make usage of it:
class User < ApplicationRecord
has_one :location
def address
location || NullLocation.new
end
end
You can now fetch the full version of the address without worrying about the object persistence:
user = User.find(1)
user.address.full
Word at the end
I haven’t mentioned all the design patterns that are used as there are plenty of them. Some of them are more useful; some are less. Any design pattern should be used with caution. When using them not correctly, we can harm our architecture and overcomplicate the code, which leads to longer development time and higher technical debt.