Skip to content
← All posts
5 min read Dawid Skłodowski

Decorators and presenters for clean views

View logic has nowhere good to live in Rails — it leaks into models and clutters templates. A deep-dive on decorators and presenters: the difference, when to use each, plain PORO vs Draper, and how to keep presentation out of your domain.

Every Rails app accumulates presentation logic — the code that decides how a model is shown: a formatted price, a CSS class for a status, “posted 3 days ago”, a full name assembled from parts. Rails gives this code no obvious home, so it leaks into two bad places. It goes into the model (def display_name; "#{first} #{last}".strip; end), bloating the domain object with concerns about how it looks on a page. Or it goes into the view as a tangle of if/else and inline formatting that is impossible to test and duplicated across templates. Decorators and presenters give this logic a proper home. Here is how to use them.

The problem, concretely

Consider a product status shown with a label and a colour. Without a home for presentation, the view fills with logic:

<% if product.stock > 10 %>
  <span class="badge badge--green">In stock</span>
<% elsif product.stock > 0 %>
  <span class="badge badge--amber">Low stock (<%= product.stock %> left)</span>
<% else %>
  <span class="badge badge--red">Out of stock</span>
<% end %>

This is presentation logic — the rules for how a product looks — sitting in a template where it cannot be unit-tested and gets copy-pasted into every page that shows stock. The instinct to fix it by moving stock_label onto the Product model is also wrong: the model now knows about badges and CSS, mixing domain logic with presentation. Neither the model nor the view is the right place. A presentation object is.

Decorator vs presenter: the distinction

The two terms are used loosely, but the useful distinction is this:

  • A decorator wraps a single model instance and adds presentation methods to it. It quacks like the model — product.name still works — but also offers product.stock_label. It is the model “dressed up” for the view.
  • A presenter is an object built for a specific view or screen, often pulling together several models and the data a particular page needs. It is less “a decorated record” and more “the view-model for this page”.

In practice the decorator handles “how do I display this record” and the presenter handles “what does this page need”. Both keep presentation out of the model and out of the template; which you reach for depends on whether your logic is about one record or one screen.

A plain-Ruby decorator

You do not need a gem to start. A decorator can be a plain object that wraps the model:

class ProductDecorator < SimpleDelegator
  def stock_label
    case __getobj__.stock
    when 11..Float::INFINITY then badge("In stock", :green)
    when 1..10               then badge("Low stock (#{stock} left)", :amber)
    else                          badge("Out of stock", :red)
    end
  end

  private

  def badge(text, colour)
    %(<span class="badge badge--#{colour}">#{text}</span>).html_safe
  end
end

SimpleDelegator forwards everything to the wrapped object, so the decorator is a product for all other purposes — decorated.name, decorated.price all work — while adding stock_label. The controller wraps the record and the view stays clean:

@product = ProductDecorator.new(Product.find(params[:id]))
<%= @product.stock_label %>

Now the badge logic is a method you can unit-test directly — instantiate the decorator with a stock value and assert the output — with no view, no request. That testability is the whole point: presentation logic deserves tests as much as any other code, and in a template it gets none.

Draper: the conventional gem

For a consistent, batteries-included approach, Draper is the established Rails gem. It gives decorators a convention (ProductDecorator < Draper::Decorator), a decorate helper, automatic collection decoration, and — usefully — access to view helpers and routes inside the decorator (h.number_to_currency, h.product_path), which a plain PORO does not get for free:

class ProductDecorator < Draper::Decorator
  delegate_all

  def price
    h.number_to_currency(object.price)        # view helpers available via `h`
  end

  def stock_label
    h.content_tag(:span, status_text, class: "badge badge--#{status_colour}")
  end
end
@product = Product.find(params[:id]).decorate

Draper’s access to helpers and routes is its main advantage over a hand-rolled SimpleDelegator — building HTML and links is what presentation methods do constantly, and having h available keeps them clean. The trade-off is a dependency and a little magic; for a small app a plain PORO is often enough, while a larger app benefits from Draper’s consistency.

Decorators vs helpers

Rails already has helpers for view logic, so when do you use a decorator instead? The distinction that has served us: helpers are good for generic, stateless functions that are not tied to a particular object — format_date, icon_for, a generic button builder. Decorators are good for presentation that belongs to a specific model — product.stock_label, user.display_name. The smell that pushes you from helpers to decorators is a helper that takes a model and pokes at its attributes (def stock_label(product); product.stock > 10 ? ... ; end) — that is really a method on the product’s presentation, and it reads far better as product.stock_label. Helpers that are really object methods in disguise are exactly what decorators clean up.

Don’t over-build

The usual caveat. Not every model needs a decorator, and a decorator with one trivial method is ceremony. Reach for one when a model accumulates several presentation concerns, or when the same display logic appears in more than one place, or when view logic has grown complex enough to deserve tests. For a one-off bit of formatting on a single page, a helper — or even a small amount of view logic — is fine. The goal is to give presentation a testable home when it earns one, not to wrap everything reflexively.

Verdict

Presentation logic is real code that deserves a real home, and in Rails that home is a decorator or presenter — not the model (which should not know about CSS) and not the template (which cannot be tested). Use a decorator to dress up a single record with display methods, a presenter to assemble what a whole screen needs; start with a plain SimpleDelegator and graduate to Draper when you want helper access and convention; and prefer decorators over helpers when the logic clearly belongs to a specific object. Apply it where presentation has grown enough to deserve tests, and your models stay focused on the domain, your views stay readable, and the rules for how things look become code you can actually verify.