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.