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