Name the Abstraction, Not the Data

Eebs Kobeissi

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.