7 Design Patterns to Refactor MVC Components in Rails

Share this article

7 Design Patterns to Refactor MVC Components in Rails
dphead

In our previous post, The Basics of MVC in Rails, we discussed theoretical aspects of the MVC design pattern. We defined what MVC stands for, identified what each MVC component is responsible for, addressed what happens when a component contains redundant logic, and, most importantly, we introduced the concept of refactoring. In this article, as promised, we’ll show you each design pattern at work.

Service Objects (and Interactor Objects)

Service Objects are created when an action:

  • is complex (such as calculating an employee’s salary)
  • uses APIs of external services
  • clearly doesn’t belong to one model (for example, deleting outdated data)
  • uses several models (for example, importing data from one file to several models)

Example

In the example below, work is performed by the external Stripe service. The Stripe service creates a Stripe customer based on an email address and a source (token), and ties any service payment to this account.

Problem

  • The logic of operation with an external service is located in the Controller.
  • The Controller forms data for an external service.
  • It’s difficult to maintain and scale the Controller.
class ChargesController < ApplicationController
  def create
    amount = params[:amount].to_i * 100

    customer = Stripe::Customer.create(
       email: params[:email],
       source: params[:source]
    )
    charge = Stripe::Charge.create(
      customer: customer.id,
      amount: amount,
      description: params[:description],
      currency: params[:currency] || 'USD'
    )
    redirect_to charges_path
  rescue Stripe::CardError => exception
    flash[:error] = exception.message
    redirect_to new_charge_path
  end
end

To solve these issues, we encapsulate our work with an external service.

class ChargesController < ApplicationController
 def create
   CheckoutService.new(params).call
   redirect_to charges_path
 rescue Stripe::CardError => exception
   flash[:error] = exception.message
   redirect_to new_charge_path
 end
end

class CheckoutService
 DEFAULT_CURRENCY = 'USD'.freeze

 def initialize(options = {})
   options.each_pair do |key, value|
     instance_variable_set("@#{key}", value)
   end
 end

 def call
   Stripe::Charge.create(charge_attributes)
 end

 private

 attr_reader :email, :source, :amount, :description

 def currency
   @currency || DEFAULT_CURRENCY
 end

 def amount
   @amount.to_i * 100
 end

 def customer
   @customer ||= Stripe::Customer.create(customer_attributes)
 end

 def customer_attributes
   {
     email: email,
     source: source
   }
 end

 def charge_attributes
   {
     customer: customer.id,
     amount: amount,
     description: description,
     currency: currency
   }
 end
end

The result is a CheckoutService that’s responsible for customer account creation and payment. But, having solved the problem of too much logic in the Controller, we still have another problem to solve. What happens if an external service throws an exception (for example, when a credit card is invalid) and we have to redirect the user to another page?

class ChargesController < ApplicationController
 def create
   CheckoutService.new(params).call
   redirect_to charges_path
 rescue Stripe::CardError => exception
   flash[:error] = exception.error
   redirect_to new_charge_path
 end
end

To handle this scenario, we include a CheckoutService call and intercept exceptions with the Interactor Object. Interactors are used to encapsulate business logic. Each interactor usually describes one business rule.

The Interactor pattern helps us achieve the Single Responsibility Principle (SRP) by using plain old Ruby objects (POROs) – leaving models responsible only at the persistence level. Interactors are similar to Service Objects, but generally return several values that show the state of execution and other information (in addition to executing actions). It’s also common practice to use Service Objects inside Interactor Objects. Here’s an example of this design pattern usage:

class ChargesController < ApplicationController
 def create
   interactor = CheckoutInteractor.call(self)

   if interactor.success?
 redirect_to charges_path
   else
 flash[:error] = interactor.error
 redirect_to new_charge_path
   end
 end
end

class CheckoutInteractor
 def self.call(context)
   interactor = new(context)
   interactor.run
   interactor
 end

 attr_reader :error

 def initialize(context)
   @context = context
 end

 def success?
   @error.nil?
 end

 def run
   CheckoutService.new(context.params)
 rescue Stripe::CardError => exception
   fail!(exception.message)
 end

 private

 attr_reader :context

 def fail!(error)
   @error = error
 end
end

Moving all exceptions that are related to card errors, we can achieve a skinny Controller. Now, the Controller is responsible only for redirecting a user to pages for successful payment or unsuccessful payment.

Value Objects

The Value Object design pattern encourages simple, small objects (which usually just contain given values), and lets you compare these objects according to a given logic or simply based on specific attributes (and not on their identity). Examples of values objects are objects representing money values in various currencies. We could then compare these value objects using one currency (e.g. USD). Value objects could also represent temperatures and be compared using the Kelvin scale, for instance.

Example

Let’s assume we have a smart home with electric heat, and the heaters are controlled through a web interface. A Controller action receives parameters for a given heater from a temperature sensor: the temperature (as a numerical value) and the temperature scale (Fahrenheit, Celsius, or Kelvin). This temperature is converted to Kelvin if provided in another scale, and the Controller checks whether the temperature is less than 25°C and whether it is equal to or greater than the current temperature.

Problem

The Controller contains too much logic related to conversion and comparison of temperature values.

class AutomatedThermostaticValvesController < ApplicationController
  SCALES = %w(kelvin celsius fahrenheit)
  DEFAULT_SCALE = 'kelvin'
  MAX_TEMPERATURE = 25 + 273.15

  before_action :set_scale

  def heat_up
    was_heat_up = false
    if previous_temperature < next_temperature && next_temperature < MAX_TEMPERATURE
      valve.update(degrees: params[:degrees], scale: params[:scale])
      Heater.call(next_temperature)
      was_heat_up = true
    end
    render json: { was_heat_up: was_heat_up }
  end

  private

  def previous_temperature
    kelvin_degrees_by_scale(valve.degrees, valve.scale)
  end

  def next_temperature
    kelvin_degrees_by_scale(params[:degrees], @scale)
  end

  def set_scale
    @scale = SCALES.include?(params[:scale]) ? params[:scale] : DEFAULT_SCALE
  end

  def valve
    @valve ||= AutomatedThermostaticValve.find(params[:id])
  end

  def kelvin_degrees_by_scale(degrees, scale)
    degrees = degrees.to_f
    case scale.to_s
    when 'kelvin'
      degrees
    when 'celsius'
      degrees + 273.15
    when 'fahrenheit'
      (degrees - 32) * 5 / 9 + 273.15
    end
  end
end

We moved the temperature comparison logic to the Model, so the Controller just passes parameters to the update method. But the Model still isn’t ideal — it knows too much about how to handle temperature conversions.

class AutomatedThermostaticValvesController < ApplicationController
  def heat_up
    valve.update(next_degrees: params[:degrees], next_scale: params[:scale])

    render json: { was_heat_up: valve.was_heat_up }
  end

  private

  def valve
    @valve ||= AutomatedThermostaticValve.find(params[:id])
  end
end

class AutomatedThermostaticValve < ActiveRecord::Base
  SCALES = %w(kelvin celsius fahrenheit)
  DEFAULT_SCALE = 'kelvin'

  before_validation :check_next_temperature, if: :next_temperature
  after_save :launch_heater, if: :was_heat_up

  attr_accessor :next_degrees, :next_scale
  attr_reader :was_heat_up

  def temperature
    kelvin_degrees_by_scale(degrees, scale)
  end

  def next_temperature
    kelvin_degrees_by_scale(next_degrees, next_scale) if next_degrees.present?
  end

  def max_temperature
    kelvin_degrees_by_scale(25, 'celsius')
  end

  def next_scale=(scale)
    @next_scale = SCALES.include?(scale) ? scale : DEFAULT_SCALE
  end

  private

  def check_next_temperature
    @was_heat_up = false
    if temperature < next_temperature && next_temperature <= max_temperature
      @was_heat_up = true
      assign_attributes(
        degrees: next_degrees,
        scale: next_scale,
      )
    end
    @was_heat_up
  end

  def launch_heater
    Heater.call(temperature)
  end

  def kelvin_degrees_by_scale(degrees, scale)
    degrees = degrees.to_f
    case scale.to_s
    when 'kelvin'
      degrees
    when 'celsius'
      degrees + 273.15
    when 'fahrenheit'
      (degrees - 32) * 5 / 9 + 273.15
    end
  end
end

To make the Model skinny, we create Value Objects. When initializing, value objects take on values of degrees and scale. When comparing these objects, the spaceship method (<=>) compares their temperature, converted to Kelvin.

Received value objects also contain a to_h method for attribute mass-assignment. Value objects provide the factory methods from_kelvin, from_celsius, and from_fahrenheit for the easy creation of the Temperature object in a certain scale, e.g. Temperature.from_celsius(0) will create an object with temperature 0°C or 273°К.

class AutomatedThermostaticValvesController < ApplicationController
  def heat_up
    valve.update(next_degrees: params[:degrees], next_scale: params[:scale])
    render json: { was_heat_up: valve.was_heat_up }
  end

  private

  def valve
    @valve ||= AutomatedThermostaticValve.find(params[:id])
  end
end

class AutomatedThermostaticValve < ActiveRecord::Base
  before_validation :check_next_temperature, if: :next_temperature
  after_save :launch_heater, if: :was_heat_up

  attr_accessor :next_degrees, :next_scale
  attr_reader :was_heat_up

  def temperature
    Temperature.new(degrees, scale)
  end

  def temperature=(temperature)
    assign_attributes(temperature.to_h)
  end

  def next_temperature
    Temperature.new(next_degrees, next_scale) if next_degrees.present?
  end

  private

  def check_next_temperature
    @was_heat_up = false
    if temperature < next_temperature && next_temperature <= Temperature::MAX
      self.temperature = next_temperature
      @was_heat_up = true
    end
  end

  def launch_heater
    Heater.call(temperature.kelvin_degrees)
  end
end

class Temperature
  include Comparable
  SCALES = %w(kelvin celsius fahrenheit)
  DEFAULT_SCALE = 'kelvin'

  attr_reader :degrees, :scale, :kelvin_degrees

  def initialize(degrees, scale = 'kelvin')
    @degrees = degrees.to_f
    @scale = case scale
    when *SCALES then scale
    else DEFAULT_SCALE
    end

    @kelvin_degrees = case @scale
    when 'kelvin'
      @degrees
    when 'celsius'
      @degrees + 273.15
    when 'fahrenheit'
      (@degrees - 32) * 5 / 9 + 273.15
    end
  end

  def self.from_celsius(degrees_celsius)
    new(degrees_celsius, 'celsius')
  end

  def self.from_fahrenheit(degrees_fahrenheit)
    new(degrees_celsius, 'fahrenheit')
  end

  def self.from_kelvin(degrees_kelvin)
    new(degrees_kelvin, 'kelvin')
  end

  def <=>(other)
    kelvin_degrees <=> other.kelvin_degrees
  end

  def to_h
    { degrees: degrees, scale: scale }
  end

  MAX = from_celsius(25)
end

The result is a skinny Controller and a skinny Model. The Controller doesn’t know anything about temperature-related logic, and the Model doesn’t know anything about temperature conversions either, using only the methods of Temperature value objects.

Form Objects

Form Object is a design pattern that encapsulates logic related to validating and persisting data.

Example

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

Problem

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 = /@/ # Some fancy email regex

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

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

class UserForm
  EMAIL_REGEX = // # Some fancy 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

After we move the validation logic to UserForm, we can use it inside the Controller like this:

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 no longer responsible for validating data:

class User < ActiveRecord::Base
end

Query Objects

Query Object is a design pattern that lets us extract query logic from Controllers and Models into reusable classes.

Example

We want to request a list of articles with the type “video” that have a view count greater than 100 and that the current user can access.

Problem

All query logic is in the Controller (all query conditions are imposed in the Controller);

  • This logic isn’t reusable
  • It’s hard to test
  • Any changes to the article scheme can break this code
class Article < ActiveRecord::Base
    # t.string :status
    # t.string :type
    # t.integer :view_count
  end

 class ArticlesController < ApplicationController
    def index
      @articles = Article
                  .accessible_by(current_ability)
                  .where(type: :video)
                  .where('view_count > ?', 100)
    end
  end

Our first step in refactoring this controller would be to hide and encapsulate underlying query conditions and provide a simple API for the query model. In Rails, we can do this by creating scopes:

class Article < ActiveRecord::Base
  scope :with_video_type, -> { where(type: :video) }
  scope :popular, -> { where('view_count > ?', 100) }
  scope :popular_with_video_type, -> { popular.with_video_type }
end

Now we can use this simple API to query everything we need without worrying about the underlying implementation. If our article scheme changes, we’ll only need to make changes to the article class:

class ArticlesController < ApplicationController
  def index
    @articles = Article
                .accessible_by(current_ability)
                .popular_with_video_type
  end
end

This is far better, but now some new problems arise. We need to create scopes for every query condition we want to encapsulate, bloating the Model with different combinations of scope for different use cases. Another issue is that scopes are not reusable across different models, meaning you can’t just use the scope from the Article class to query the Attachment class. We’re also breaking the single responsibility principle by throwing all query-related responsibilities into the Article class. The solution to these problems is to use a Query Object.

class PopularVideoQuery
  def call(relation)
    relation
      .where(type: :video)
      .where('view_count > ?', 100)
  end
end

class ArticlesController < ApplicationController
  def index
    relation = Article.accessible_by(current_ability)
    @articles = PopularVideoQuery.new.call(relation)
  end
end

Wow, it’s reusable! Now we can use this class to query any other repositories that have a similar scheme:

class Attachment < ActiveRecord::Base
  # t.string :type
  # t.integer :view_count
end

PopularVideoQuery.new.call(Attachment.all).to_sql
# "SELECT \"attachments\".* FROM \"attachments\" WHERE \"attachments\".\"type\" = 'video' AND (view_count > 100)"
PopularVideoQuery.new.call(Article.all).to_sql
# "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"type\" = 'video' AND (view_count > 100)"

Also, if we want to chain them, it’s simple. The only thing we have to take into account is that the call method should conform to the interfaces of ActiveRecord::Relation:

class BaseQuery
  def |(other)
    ChainedQuery.new do |relation|
      other.call(call(relation))
    end
  end
end

class ChainedQuery < BaseQuery
  def initialize(&block)
    @block = block
  end

  def call(relation)
    @block.call(relation)
  end
end

class WithStatusQuery < BaseQuery
  def initialize(status)
    @status = status
  end

  def call(relation)
    relation.where(status: @status)
  end
end

query = WithStatusQuery.new(:published) | PopularVideoQuery.new
query.call(Article.all).to_sql
# "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"status\" = 'published' AND \"articles\".\"type\" = 'video' AND (view_count > 100)"

We now have a reusable class with all query logic encapsulated, with a simple interface, that’s easy to test.

View Objects (Serializer/Presenter)

A View Object allows us to take data and calculations that are needed only for surfacing a representation of the Model in the View – such as an HTML page for a website or a JSON response from an API endpoint – out of the Controller and Model.

Example

There are various actions (calculations) happening in the View. The View:

  • Creates an image URL from protocol host and image path
  • Takes an article title and description; if there are not any custom values, it takes default ones
  • Concatenates first name and last name to display full name
  • Applies the right formatting for article creation date

Problem

The View contains too much calculation logic.

#before refactoring
#/app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
 def show
   @article = Article.find(params[:id])
 end
end

#/app/views/articles/show.html.erb
<% content_for :header do %>
 <title>
     <%= @article.title_for_head_tag || I18n.t('default_title_for_head') %>
 </title>
 <meta name='description' content="<%= @article.description_for_head_tag || I18n.t('default_description_for_head') %>">
  <meta property="og:type" content="article">
  <meta property="og:title" content="<%= @article.title %>">
  <% if @article.description_for_head_tag %>
    <meta property="og:description" content="<%= @article.description_for_head_tag %>">
  <% end %>
  <% if @article.image %>
     <meta property="og:image" content="<%= "#{request.protocol}#{request.host_with_port}#{@article.main_image}" %>">
  <% end %>
<% end %>

<% if @article.image %>
 <%= image_tag @article.image.url %>
<% else %>
 <%= image_tag 'no-image.png'%>
<% end %>
<h1>
 <%= @article.title %>
</h1>

<p>
 <%= @article.text %>
</p>

<% if @article.author %>
<p>
 <%= "#{@article.author.first_name} #{@article.author.last_name}" %>
</p>
<%end%>

<p>
 <%= t('date') %>
 <%= @article.created_at.strftime("%B %e, %Y")%>
</p>

To solve this problem, we create a basic presenter class and then create an ArticlePresenter class instance. ArticlePresenter methods return the desired tags with appropriate calculations:

#/app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
 def show
   @article = Article.find(params[:id])
 end
end

#/app/presenters/base_presenter.rb
class BasePresenter
 def initialize(object, template)
   @object = object
   @template = template
 end

 def self.presents(name)
   define_method(name) do
     @object
   end
 end
  def h
   @template
 end
end

#/app/helpers/application_helper.rb
module ApplicationHelper
  def presenter(model)
    klass = "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

#/app/presenters/article_presenters.rb
class ArticlePresenter < BasePresenter
 presents :article
 delegate :title, :text, to: :article

 def meta_title
   title = article.title_for_head_tag || I18n.t('default_title_for_head')
   h.content_tag :title, title
 end

 def meta_description
   description = article.description_for_head_tag || I18n.t('default_description_for_head')
   h.content_tag :meta, nil, content: description
 end

 def og_type
   open_graph_meta "article", "og:type"
 end
  def og_title
   open_graph_meta "og:title", article.title
 end

 def og_description
   open_graph_meta "og:description", article.description_for_head_tag if article.description_for_head_tag
 end
  def og_image
   if article.image
     image = "#{request.protocol}#{request.host_with_port}#{article.main_image}"
     open_graph_meta "og:image", image
   end
 end

 def author_name
   if article.author
     h.content_tag :p, "#{article.author.first_name} #{article.author.last_name}"
   end
 end

 def image
  if article.image
    h.image_tag article.image.url
  else
     h.image_tag 'no-image.png'
  end
 end
  private
  def open_graph_meta content, property
   h.content_tag :meta, nil, content: content, property: property
 end
end

Great! We have a View that doesn’t include any logic related to calculations, and all the components have been moved out to the presenter and are reusable in other Views, like so:

#/app/views/articles/show.html.erb
<% presenter @article do |article_presenter| %>
 <% content_for :header do %>
   <%= article_presenter.meta_title %>
   <%= article_presenter.meta_description %>
   <%= article_presenter.og_type %>
   <%= article_presenter.og_title %>
   <%= article_presenter.og_description %>
   <%= article_presenter.og_image %>
 <% end %>
 <%= article_presenter.image%>
 <h1> <%= article_presenter.title %> </h1>
 <p>  <%= article_presenter.text %> </p>
 <%= article_presenter.author_name %>
<% end %>

Policy Objects

The Policy Objects design pattern is similar to Service Objects, but is responsible for read operations while Service Objects are responsible for write operations. Policy Objects encapsulate complex business rules and can easily be replaced by other Policy Objects with different rules. For example, we can check if a guest user is able to retrieve certain resources using a guest Policy Object. If the user is an admin, we can easily change this guest Policy Object to an admin Policy Object that contains admin rules.

Example

Before a user creates a project, the Controller checks whether the current user is a manager, whether they have permission to create a project, whether the number of current user projects is under the maximum, and checks the presence of blocks on the creation of projects in Redis key/value storage.

Problem

  • Only the Controller knows about policies of project creation
  • The Controller contains excessive logic
class ProjectsController < ApplicationController
   def create
     if can_create_project?
       @project = Project.create!(project_params)
       render json: @project, status: :created
     else
       head :unauthorized
     end
   end

  private

  def can_create_project?
     current_user.manager? &&
       current_user.projects.count < Project.max_count &&
       redis.get('projects_creation_blocked') != '1'
   end

  def project_params
     params.require(:project).permit(:name, :description)
   end

  def redis
      Redis.current
    end
  end

  def User < ActiveRecord::Base
     enum role: [:manager, :employee, :guest]
  end

To make the Controller skinny, we move policy logic to the Model. As a result, the check is entirely moved out of the Controller. But now the User class knows about Redis and the Project class logics, and so the Model becomes fat.

def User < ActiveRecord::Base
  enum role: [:manager, :employee, :guest]

  def can_create_project?
    manager? &&
      projects.count < Project.max_count &&
        redis.get('projects_creation_blocked') != '1'
  end

  private

  def redis
    Redis.current
  end
end

class ProjectsController < ApplicationController
  def create
    if current_user.can_create_project?
       @project = Project.create!(project_params)
       render json: @project, status: :created
    else
       head :unauthorized
    end
  end

  private

  def project_params
     params.require(:project).permit(:name, :description)
  end
end

In this case, we can make the Model and Controller skinny by moving this logic to a policy object:

class CreateProjectPolicy
  def initialize(user, redis_client)
    @user = user
    @redis_client = redis_client
  end

  def allowed?
    @user.manager? && below_project_limit && !project_creation_blocked
  end

 private

  def below_project_limit
    @user.projects.count < Project.max_count
  end

  def project_creation_blocked
    @redis_client.get('projects_creation_blocked') == '1'
  end
end

class ProjectsController < ApplicationController
  def create
    if policy.allowed?
       @project = Project.create!(project_params)
       render json: @project, status: :created
     else
       head :unauthorized
     end
  end

  private

  def project_params
     params.require(:project).permit(:name, :description)
  end

  def policy
     CreateProjectPolicy.new(current_user, redis)
  end

  def redis
     Redis.current
  end
end

def User < ActiveRecord::Base
   enum role: [:manager, :employee, :guest]
end

The result is a clean Controller and Model. The policy object encapsulates the permission check logic, and all external dependencies are injected from the Controller into the policy object. All classes do their own work and no one else’s.

Decorators

The Decorator Pattern allows us to add any kind of auxiliary behavior to individual objects without affecting other objects of the same class. This design pattern is widely used to divide functionality across different classes, and is a good alternative to subclasses for adhering to the Single Responsibility Principle.

Example

Let’s assume there are many actions (calculations) happening in the View:

  • The title is displayed differently depending on the presence of title_for_head value
  • The View displays a default car image when not provided with a custom car image URL
  • The View displays default text when values are undefined for brand, model, notes, owner, city, and owner phone
  • The View shows a text representation of the car’s state
  • The View displays formatted car object date of creation

Problem

The View contains too much calculation logic.

#/app/controllers/cars_controller.rb
class CarsController < ApplicationController
 def show
   @car = Car.find(params[:id])
 end
end

#/app/views/cars/show.html.erb
<% content_for :header do %>
 <title>
   <% if @car.title_for_head %>
     <%="#{ @car.title_for_head } | #{t('beautiful_cars')}" %>
   <% else %>
     <%= t('beautiful_cars') %>
   <% end %>
 </title>
 <% if @car.description_for_head%>
   <meta name='description' content= "#{<%= @car.description_for_head %>}">
 <% end %>
<% end %>

<% if @car.image %>
 <%= image_tag @car.image.url %>
<% else %>
 <%= image_tag 'no-images.png'%>
<% end %>
<h1>
 <%= t('brand') %>
 <% if @car.brand %>
   <%= @car.brand %>
 <% else %>
    <%= t('undefined') %>
 <% end %>
</h1>

<p>
 <%= t('model') %>
 <% if @car.model %>
   <%= @car.model %>
 <% else %>
    <%= t('undefined') %>
 <% end %>
</p>

<p>
 <%= t('notes') %>
 <% if @car.notes %>
   <%= @car.notes %>
 <% else %>
    <%= t('undefined') %>
 <% end %>
</p>

<p>
 <%= t('owner') %>
 <% if @car.owner %>
   <%= @car.owner %>
 <% else %>
    <%= t('undefined') %>
 <% end %>
</p>

<p>
 <%= t('city') %>
 <% if @car.city %>
   <%= @car.city %>
 <% else %>
    <%= t('undefined') %>
 <% end %>
</p>
<p>
 <%= t('owner_phone') %>
 <% if @car.phone %>
   <%= @car.phone %>
 <% else %>
    <%= t('undefined') %>
 <% end %>
</p>

<p>
 <%= t('state') %>
 <% if @car.used %>
   <%= t('used') %>
 <% else %>
   <%= t('new') %>
 <% end %>
</p>

<p>
 <%= t('date') %>
 <%= @car.created_at.strftime("%B %e, %Y")%>
</p>

We can address this problem with the Draper decorating gem, which moves all logic to CarDecorator methods:

#/app/controllers/cars_controller.rb
class CarsController < ApplicationController
 def show
   @car = Car.find(params[:id]).decorate
 end
end

#/app/decorators/car_decorator.rb
class CarDecorator < Draper::Decorator
 delegate_all
 def meta_title
   result =
     if object.title_for_head
       "#{ object.title_for_head } | #{I18n.t('beautiful_cars')}"
     else
       t('beautiful_cars')
     end
   h.content_tag :title, result
 end

 def meta_description
   if object.description_for_head
     h.content_tag :meta, nil ,content: object.description_for_head
   end
 end

 def image
   result = object.image.url.present? ? object.image.url : 'no-images.png'
   h.image_tag result
 end

 def brand
   get_info object.brand
 end

 def model
   get_info object.model
 end

 def notes
   get_info object.notes
 end

 def owner
   get_info object.owner
 end

 def city
   get_info object.city
 end

 def owner_phone
   get_info object.phone
 end

 def state
   object.used ? I18n.t('used') : I18n.t('new')
 end

 def created_at
   object.created_at.strftime("%B %e, %Y")
 end

 private

 def get_info value
   value.present? ? value : t('undefined')
 end
end

The result is a neat View without any calculations:

#/app/views/cars/show.html.erb
<% content_for :header do %>
 <%= @car.meta_title %>
 <%= @car.meta_description%>
<% end %>
​
<%= @car.image %>
<h1> <%= t('brand') %> <%= @car.brand %> </h1>
<p> <%= t('model') %> <%= @car.model %>  </p>
<p> <%= t('notes') %> <%= @car.notes %>  </p>
<p> <%= t('owner') %> <%= @car.owner %>  </p>
<p> <%= t('city') %> <%= @car.city %>    </p>
<p> <%= t('owner_phone') %> <%= @car.phone %> </p>
<p> <%= t('state') %> <%= @car.state %>   </p>
<p> <%= t('date') %> <%= @car.created_at%> </p>

Wrapping Up

These concepts should provide you with a basic understanding of when and how you can refactor your code. There are many tools for managing code complexity. By carefully placing your logic from the beginning of development, you can reduce the amount of time you need to spend refactoring.

Frequently Asked Questions on Refactoring MVC Components in Rails

What are the benefits of using design patterns in Rails?

Design patterns in Rails provide a reusable solution to commonly occurring problems in software design. They offer a way to structure your code in a manner that is efficient, scalable, and easy to understand. By using design patterns, you can reduce the complexity of your code, making it easier to maintain and debug. They also improve code readability, which is crucial when working in a team environment. Furthermore, design patterns can enhance the performance of your application by reducing unnecessary database queries and optimizing code execution.

How does the Presenter pattern improve MVC components in Rails?

The Presenter pattern is a design pattern that helps to simplify complex views by introducing an intermediate layer between the model and the view. This pattern is particularly useful when the view needs to display data that involves complex logic or multiple models. Instead of cluttering the view with this logic, you can encapsulate it in a Presenter. This makes the view cleaner and easier to understand, and also promotes code reusability since the same Presenter can be used across multiple views.

Can you explain the Service Objects pattern and its benefits?

Service Objects is a design pattern that encapsulates business logic that doesn’t neatly fit into models or controllers. It’s a way to organize your code related to a specific business process. The main benefit of using Service Objects is that it helps to keep your controllers and models lean and focused. Instead of having a model or controller that is responsible for many different things, you can delegate specific tasks to Service Objects. This makes your code easier to maintain and test.

What is the Form Object pattern and when should it be used?

The Form Object pattern is a design pattern that encapsulates the logic of complex forms. It’s used when a form involves multiple models or complex validation logic. Instead of putting this logic in the controller or the model, you can encapsulate it in a Form Object. This makes your code cleaner and easier to understand. It also makes it easier to test the form’s behavior independently from the controller or the model.

How does the Query Object pattern improve database queries in Rails?

The Query Object pattern is a design pattern that encapsulates complex database queries. Instead of scattering these queries across your models or controllers, you can encapsulate them in a Query Object. This makes your code cleaner and easier to understand. It also promotes code reusability since the same Query Object can be used across multiple models or controllers. Furthermore, it makes it easier to optimize your database queries, as all the query logic is in one place.

Can you explain the Decorator pattern and its benefits?

The Decorator pattern is a design pattern that allows you to add new behavior to an object without modifying its existing behavior. In Rails, this pattern is often used to add presentation logic to a model. Instead of cluttering the model with this logic, you can encapsulate it in a Decorator. This makes your model cleaner and easier to understand. It also promotes code reusability since the same Decorator can be used across multiple views.

What is the Policy Object pattern and when should it be used?

The Policy Object pattern is a design pattern that encapsulates authorization rules. It’s used when the authorization logic is complex or involves multiple models. Instead of putting this logic in the controller or the model, you can encapsulate it in a Policy Object. This makes your code cleaner and easier to understand. It also makes it easier to test the authorization rules independently from the controller or the model.

How does the Value Object pattern improve data handling in Rails?

The Value Object pattern is a design pattern that encapsulates a small piece of data with specific behavior. In Rails, this pattern is often used to handle complex data types or calculations. Instead of scattering this logic across your models or controllers, you can encapsulate it in a Value Object. This makes your code cleaner and easier to understand. It also promotes code reusability since the same Value Object can be used across multiple models or controllers.

Can you explain the Command pattern and its benefits?

The Command pattern is a design pattern that encapsulates a request as an object. In Rails, this pattern is often used to handle complex controller actions or background jobs. Instead of putting this logic in the controller or the model, you can encapsulate it in a Command. This makes your code cleaner and easier to understand. It also makes it easier to test the request’s behavior independently from the controller or the model.

What is the Null Object pattern and when should it be used?

The Null Object pattern is a design pattern that provides a default behavior for null or undefined objects. It’s used when you want to avoid null checks in your code. Instead of checking if an object is null before calling a method on it, you can use a Null Object that implements the same interface as the real object. This makes your code cleaner and easier to understand. It also makes it easier to handle null or undefined objects in a consistent way.

Viktoria KotsurenkoViktoria Kotsurenko
View Author

Viktoria Kotsurenko is a technical writer at RubyGarage and a fan of Ruby and Rails. She loves writing about web development, web design, testing, and other tech topics. Outside of the office, Viktoria rides her bike and takes walks in the evenings.

design patternsGlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form