Ruby on Rails

A Look into MVC Design Pattern

Author: Zain Rauf

The main principle of software development is to keep the code DRY (Don’t Repeat Yourself) to reduce the repetition within the code. So, Ruby on rails has some design patterns that allow us to reuse our code. Though we'll use models, helpers, and concerns to make the code dry, if the code complexity is higher or we are using external API, we use Rails Design Patterns. 

Here are a few design patterns that you can follow with Ruby on Rails:

  1. Service Objects
  2. Decorators
  3. View Objects
  4. Query Objects
  5. Form Objects

Service Objects

Service is the most used design pattern in Rails applications. You can use your service in multiple places, like models, controllers, jobs, etc, keeping your application clean and DRY.

When to use service objects?

  1. If a certain part of business logic doesn’t really fit into a model or controller.
  2. When we want to import CSV that contains the bulk of data.

Default Rails app structure doesn’t have any directory where we can put our services. 

Following are the steps to create one

  1. Create an app/services directory.
  2. Add service_name.rb in app/services directory

A simple service file looks like this.

class ServiceName
 attr_accessor :first_variable, :second_variable
 def initialize(first_variable, second_variable)
   @first_variable = first_variable
   @second_variable = second_variable
 end
 def call
   method_name
 end
 private
 def method_name
   # method body
 end
end

Methods initialize and call are a must-have. If the call method would be too long or complicated, it’s best to create additional methods in the private section. That’s because only the call method should be available outside of the service.

Decorator

In OOP, the decorator gives us the ability to increase a particular object’s behavior by equipping it with some additional methods.

Decorators can be useful for cleaning up logic/code written inside the view and controller in a Rails application. This design pattern is being used with a draper gem. 

Draper is beneficial once we have methods in models, which are used only in views.

Let’s have a glance at the below example:

If in a User model we have a method full_name.

def full_name
 "#{first_name} #{last_name}"
end

We should move it into a UserDecorator.

Following are the steps to use a decorator 

  1. Create an app/decorators directory.
  2. Add user_decorator.rb in app/decorators directory
class UserDecorator < Draper::Decorator
 delegate_all
    def full_name
        "#{object.first_name} #{object.last_name}"
    end
end

Great thing is that not every user object in views will have that method, We've to embellish that object before sending it to the view, simply using user.decorate. So in the controller, we've to use:

def show
 @user = User.find(params[:id].to_i).decorate
end

And then, we will use this code in our views:

<%= @user.full_name %>


View Objects

View Objects allow us to encapsulate all view related logic and keep both models and views neat. The View shouldn't contain calculation logic. To unravel the calculation logic problem, we'll use a rails helper, but if the complexity of the code is high, therein case, we should always use the Presenter.

Let’s have a glance at the below example:

<td><%= "#{@user.first_name} #{@user.last_name}" %></td>
<td><%= link_to "View more", user, class: "text-#{user.active? ? 'green' : 'red'} border-#{user.active? ? 'green' : 'red'}" %></td>

Here we'll see that we are concatenating the user’s first_name and last_name on the view. Which isn't the best practice. Hence to unravel this problem, we should always use the presenter.

Following are the steps to use Presenter:

  1. Create an app/presenters folder.
  2. Add user_presenter.rb in app/presenters folder
class UserPresenter
 def initialize(user)
   @user = user
 end
 def full_name
   "#{@user.first_name} #{@user.last_name}"
 end
 def css_color
   @user.active? ? 'green' : 'red'
 end
end

Now let’s change the view to make it more readable and with no calculations.

<% presenter = UserPresenter.new(user) %>
<td><%= presenter.full_name %></td>
<td><%= link_to "View more", user, class: "text-#{presenter.css_color} border-#{presenter.css_color}" %></td>


Query Objects

Query Object could also be a kind of design pattern that lets us fetch query logic from Controllers and Models into reusable classes.

Let’s have a glance at the below example:

class ActivityLogsController < ApplicationController
 def index
   @activity_logs = ActivityLog.where(is_important: true).order(created_at: :desc)
 end
end

The problems in the above code:

  1. This code isn’t reusable
  2. It’s hard to test the logic.
  3. Any changes to the post schema can break this logic/code.

To make the controller skinny, readable, and neat we will use scopes:

class ActivityLog < ApplicationRecord
  scope :by_most_recent, -> { order(created_at: :desc) }
  scope :marked_important, -> { where(is_important: true) }
end

So, the controller will appear something like this:

class ActivityLogsController < ApplicationController
 def index
   @activity_logs = ActivityLog.marked_important.by_most_recent
 end
end

Form Objects

Form Object is a design pattern that encapsulates logic associated with validating and persisting data.

Let’s assume we’ve got a typical Rails Model and Controller action for creating new users.

The Model contains all validation logic, so it’s not reusable for other entities, e.g. Admin

class UsersController < ApplicationController
 def create
   @user = User.new(user_params)
   if @user.save
     render json: @user
   else
     render json: @user.error, status: :unprocessable_entity
   end
 end
 private
 def user_params
   params.require(:user).permit(:email, :full_name, :password, :password_confirmation)
 end
end
class User < ActiveRecord::Base
 EMAIL_REGEX = /@/
 validates :full_name, presence: true
 validates :email, presence: true, format: EMAIL_REGEX
 validates :password, presence: true, confirmation: true
end

The better solution is to move the validation logic to a separate singular responsibility class that we might call UserForm:

class UserForm
 EMAIL_REGEX = /@/
 include ActiveModel::Model
 include Virtus.model
 attribute :id, Integer
 attribute :full_name, String
 attribute :email, String
 attribute :password, String
 attribute :password_confirmation, String

 validates :full_name, presence: true
 validates :email, presence: true, format: EMAIL_REGEX
 validates :password, presence: true, confirmation: true

 attr_reader :record

 def persist
   @record = id ? User.find(id) : User.new
   if valid?
     @record.attributes = attributes.except(:password_confirmation, :id)
     @record.save!
     true
   else
     false
   end
 end
end

Now, We can use it inside our users_controller like that:

class UsersController < ApplicationController
 def create
   @form = UserForm.new(user_params)
   if @form.persist
     render json: @form.record
   else
     render json: @form.errors, status: :unpocessably_entity
   end
 end
 private
 def user_params
   params.require(:user).permit(:email, :full_name, :password, :password_confirmation)
 end
end

As a result, the user Model is not any longer liable for validating data:

class User < ActiveRecord::Base
end


Conclusion

Design patterns in your codes can be an effective way to manage complexity. These concepts should give you a basic understanding of when and how you can refactor your code.