Rails design patterns

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.

All my notes are based on years of hands-on experience at iRonin.IT - a top software development company, where we provide custom software development and IT staff augmentation services for a wide array of technologies.

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.