VOOZH about

URL: https://www.sitepoint.com/7-design-patterns-to-refactor-mvc-components-in-rails/

⇱ 7 Design Patterns to Refactor MVC Components in Rails — SitePoint


This metrics tool terrifies bad developers

Start free trial

This metrics tool terrifies bad developers

Start free trial
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

👁 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.

Key Takeaways

  • Service Objects and Interactors streamline complex operations that don’t fit neatly into models or controllers, ensuring that each component adheres to the Single Responsibility Principle.
  • Value Objects enhance data handling by encapsulating simple entities like temperatures or currencies, allowing for easy comparison and data integrity without identity considerations.
  • Form Objects centralize form processing tasks, moving validation and saving logic out of models to make them reusable and maintainable.
  • Query Objects consolidate query logic into standalone, reusable classes that simplify modifications and enhance the maintainability of database interactions.
  • View Objects (Presenter/Serializer) extract presentation logic from models and controllers, simplifying views by delegating formatting and complex data gathering to dedicated classes.
  • Policy Objects encapsulate authorization rules, making security aspects more manageable and adaptable to changes in business rules or user roles.

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 Kotsurenko
Viktoria Kotsurenko

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.

SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

Stuff we do
Contact
About
Connect
Subscribe to our newsletter

Get the freshest news and resources for developers, designers and digital creators in your inbox each week

© 2000 – 2026 SitePoint Pty. Ltd.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
Privacy PolicyTerms of Service