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.namestill works — but also offersproduct.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.