When building web products, data takes many shapes and forms. It is our job to mold that data for displaying to users. A common solution is to reach for a Presenter or Decorator.
Consider an e-commerce application that has an Order
model. There is a details
view for each Order
where we display information such as when it was placed,
the purchaser, and the items within the Order
. We need to modify the data
within the Order
before it is ready to display in the view. For example, we
need to format the date, so it’s easier for a user to understand.
class Order < ApplicationRecord
def header_date
placed_at.to_formatted_s(:long_ordinal)
end
end
It’s easy to imagine our Order
model growing over time with view-specific
methods. These methods are not core to the Order
model, and it becomes
difficult to understand when or where to use them.
A familiar pattern is to create an OrderPresenter
.
class OrderPresenter
def initialize(order)
@order = order
end
def header_date
order.placed_at.to_formatted_s(:long_ordinal)
end
private
attr_reader :order
end
This is a good first step. It provides a place for us to consolidate our view methods and attract new ones. However, over time we will run into the same issue as before. Our generic presenter will become full of methods that different views use in different ways.
Name the abstraction
The focus of our presenter so far has been on the data it encapsulates, the
Order
. Rather than organize and name our presenters by the data they house, we
can increase clarity and specificity by naming them after the abstractions they
represent. Imagine the view which displays all the information about the order.
At the top we may have a header section which displays the order number, when it
was placed, and perhaps some other metadata. In the middle may be the items in
the order and the payment details. At the bottom may be information on returns
or leaving a review.
By looking for the abstractions within our view and naming our presenters after these abstractions, we can uncover a more meaningful organization and naming system.
Let’s see an example of this with an OrderHeader
presenter.
class OrderHeader
def initialize(order:, purchaser:)
@order = order
@purchaser = purchaser
end
def date
order.placed_at.to_formatted_s(:long_ordinal)
end
def order_number
order.number
end
def total_price
order.total_price_in_cents.to_f / 100
end
def buyer_name
[purchaser.first_name, purchaser.last_name].join(" ").squish
end
private
attr_reader :order, :purchaser
end
Use these abstractions with partials
Using these abstractions in our views becomes easier as well. If we’re using multiple presenters for a single view, we can create a partial for each presenter.
First, we set local variables for each presenter in the controller.
class OrdersController < ApplicationController
def show
order = Order.find(params[:id])
render locals: {
order_header: OrderHeader.new(order: order, purchaser: order.purchaser),
order_contents: OrderContents.new(order: order),
order_footer: OrderFooter.new(order: order),
}
end
end
The show
view renders a number of partials, passing the appropriate presenter.
<%# orders/show.html.erb %>
<%= render "header", header: order_header %>
<%= render "contents", contents: order_contents %>
<%= render "footer", footer: order_footer %>
The partial uses the presenter to display the data.
<%# orders/_header.html.erb %>
<div class="order-header">
<div class="order-header__number">
<%= t(".order_number", number: header.order_number ) %>
</div>
<div class="order-header__date-purchased">
<%= t(".date_purchased", formatted_date: header.date) %>
</div>
<div class="order-header__total-price">
<%= number_to_currency(header.total_price) %>
</div>
</div>
Naming your presenters after the abstraction they represent instead of the data they hold is a powerful way to bring clarity, organization, and decoupling to your codebase.
If you get stuck trying to find the abstractions, a helpful exercise is to look at your rendered view or mockup and draw boxes around functional areas. For example, you could draw boxes around the header, the items, the payment, and so on. Now try and name these boxes. The names for these boxes are likely close to the abstractions they represent. Don’t be afraid to try out one name and change it later.
Abstractions speak louder than data. They represent your ideas and intent rather than what you store in your database.