Simple Rails Design Patterns with Significant Impact

Simple Rails Design Patterns with Significant Impact

Small Rails design patterns

Especially in larger legacy Rails applications, it’s harder to make a meaningful refactoring without changing a lot of code. If you don’t have time for that or introducing more significant changes it’s not an option in your case, then you can try implementing smaller but yet powerful design patterns that don’t require any external gems or changing many classes at once.

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.

Variable pattern

This one may sound obvious to you as variables are the unchanging part of every application. However, there is always space for improvements as variables combined with meaningful naming can change the code without moving a single line from the method.

The problem

If you make a lot of calculations in your code, you may have experienced that problem before. The complex analysis is performed by the method, and its logic becomes unclear even for the author a few days after.

Let’s consider the case where we are working on an e-commerce application that provides an interface for selling items with discounts codes and provision for the platform owners:

class Product
  def calculate_price(order)
    ((price * order.quantity) * (1 - (order.discount_percentage / 100.0))) * 1.05
  end
end

Method’s body is not readable, and it’s hard to tell for which value 1.05 stands for. We calculate the price for the products, add the discount, and then the provision, which is 5%.

The solution

The solution is not much sophisticated; we have to extract the values from the calculation and assign it to variables with meaningful names:

class Product
  PERCENTAGE_PROVISION = 5

  def calculate_price(order)
    discount_factor = 1 - (order.discount_percentage / 100.0)
    total_price = price * order.quantity
    provision_factor = (PERCENTAGE_PROVISION / 100.0) + 1

    total_price * discount_factor * provision_factor
  end
end

We no longer need nested parentheses, and the final calculation is straightforward. Although our example calculation is quite simple, it was effortless to mess it up by placing all calculations in one line.

As the calculation is more complicated, the more benefits that simple refactoring pattern gives you.

Null object

The null object stands for the object that does not exist but should return a value. The simplest usage case for such a pattern is when the record does not exist, and we want to render a default value.

The problem

The main problem of the above situation is that we have to use if conditionals and mix the persistent and non-persistent object's logic.

A good example of such a situation is when the user can make orders, and we would like to render the date of the first order. If the user does not have any orders yet, we would like to render 'no orders yet' text instead:

class User < ApplicationRecord
  has_many :orders

  def first_order_date
    if (first_order = user.orders.first)
      first_order.created_at
    else
      'no orders yet'
    end
  end
end

This is the moment where the null object pattern is a quick and very good solution for the refactoring.

The solution

The common approach for creating null objects is to use the word null as the class prefix. In our case, we have to create the NullOrder class that will imitate the model class:

class NullOrder
  def created_at
    'no orders yet'
  end
end

The goal is to have an elementary and plain Ruby object that provides the interface with simple values. We can now update our User class and take advantage of using the NullOrder object:

class User < ApplicationRecord
  has_many :orders

  def first_order
    orders.first || NullOrder.new
  end
end

Now we can access the creation date of the first order without worrying about the object persistence:

user.first_order.created_at

The above code should be enough for demonstration purposes, but we should keep in mind the case where someone would like to call strftime on the created_at attribute. To handle such a case globally, we can introduce the NullDate object that will provide the strftime method and other methods.

Adding the next logic to the first order object is now simple as we can extend the NullOrder object each time we have to perform any action on the first order, and there is a possibility that the user didn’t make the first order yet.

Value object

Long live the pure Ruby objects! In the previous example, we introduced the null object pattern where you create a simple class without complex logic. The same rule applies in the case of value objects. As the name states, such an object's main task is to return the value and nothing else.

One of the main goals of object-oriented programming is to model the application to refer to the objects in the real world. If we have the Person model, it can implement the first_name and last_name methods, which is evident as every person has a name in the real world.

The problem

In reality, many times, we don’t follow the rules mentioned above, and we end up with a not readable code that is far from being modeled similarly to the real-world. Let’s consider the case where we have the following code:

data = []

CSV.foreach(file_name) do |row|
  email = row[0]
  name = row[1]
  first_name  = name.split(' ').first
  last_name = name.split(' ').last
  email_domain = email.split('@').last

  data << {
    first_name: first_name,
    last_name: last_name,
    email_domain: email_domain
  }
end

In the above code, we access the CSV file, and from each record, we collect the user's first name, last name, and e-mail domain. We can make this piece of code more readable and testable by using the value object pattern.

The solution

Let’s start with the email parsing and create a simple Email class that will provide the domain method:

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

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

The same way we can refactor the code for manipulating the name value:

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

  def first_name
    @value.split(' ').first
  end

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

Now we have two elementary classes that are meaningful and super easy to test and reuse in other places of the app. We can now refactor our main code:

data = []

CSV.foreach(file_name) do |row|
  email = Email.new(row[0])
  name = Name.new(row[1])

  data << {
    first_name: name.first_name,
    last_name: name.last_name,
    email_domain: email.domain
  }
end

We can finish the refactoring process at this step, and the final solution would be obvious and readable. However, if you would like to refactor it a little bit more, you can create an additional value object for the CSV row:

class PersonCsvRow
  def initialize(row)
    @row = row
  end

  def to_h
    {
      first_name: name.first_name,
      last_name: name.last_name,
      email_domain: email.domain
    }
  end

  private

  def name
    @name ||= Name.new(row[1])
  end

  def email
    Email.new(row[0])
  end
end

Our row class implementation provides the to_h method, which returns a hash with attributes, an excellent example of the duck typing. Let’s update the main code once again:

data = []

CSV.foreach(file_name) do |row|
  data << PersonCsvRow.new(row).to_h
end

Nothing left to refactor here.

Small design patterns are straightforward to introduce but bring many benefits in terms of testability and readability. If you are working on a legacy application or starting a brand new application, you should give them a try.

Didn't get enough of Ruby?

Check out our free books about Ruby to level up your skills and become a better software developer.