Rails design patterns - the big picture


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.