The Pug Automatic

What I dislike about Draper

Written March 1, 2014. Tagged Ruby on Rails, Presenters.

My team used Draper a few years back ago and its design inspired us to make poor decisions that we've come to regret.

If you want presenters in Rails (or "view models", or "decorators" as Draper calls them), just use a plain old Ruby object. Pass in the view context when you need it. Don't decorate. Don't delegate all the things.

The problem with Draper

Draper encourages you to do things like

app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
delegate_all

def published_at
h.content_tag(:time, object.published_at.strftime("%A, %B %e"))
end
end
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def show
@article = Article.first.decorate
end
end
app/views/articles/show.html.erb
<h1><%= @article.title %></h1>
<p><%= @article.published_at %></p>

I think this is problematic.

Using the name @article for what is actually an ArticleDecorator instance muddies the object responsibilities.

If you want to grep for where a certain presenter ("decorator") method is called, that's a bit difficult.

If you look at a view or partial and want to know what object you're actually dealing with, that's not clear.

Draper jumps through hoops so that Rails and its ecosystem will accept a non-model in forms, links, pagination etc. This is complexity that can break with new gems and new versions of Rails. I can't speak to the current state of things, but this abstraction had a number of leaks back when we used it.

For you to be able to use helpers and routes with Draper, there's some fairly dark magic going on. That's also complexity that might break with new versions of Rails, and no fun debugging if there are issues. This magic made it difficult for us to test in isolation, but perhaps that has improved since.

On a side note, I really disagree with the "decorator" naming choice. The decorator pattern is applicable to any component in a MVC system: you can decorate models, controllers, mailers and pretty much anything else. Using that wide name to mean only a narrow type of model decorator seems like a bad idea.

Draper also makes some poor choices in the small: accessing the decorated model as model or object instead of article hurts readability.

PORO presenters

What should you do instead? Just use plain old Ruby objects.

app/presenters/article_presenter.rb
class ArticlePresenter
def initialize(article)
@article = article
end

def published_at(view)
view.content_tag(:time, article.published_at.strftime("%A, %B %e"))
end

private

attr_reader :article
end
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def show
@article = Article.first
@article_presenter = ArticlePresenter.new(@article)
end
end
app/views/articles/show.html.erb
<h1><%= @article.title %></h1>
<p><%= @article_presenter.published_at(self) %></p>

This is not much more code than a Draper decorator.

With our attr_extras library you could even get rid of the initializer and reader boilerplate:

app/presenters/article_presenter.rb
class ArticlePresenter
pattr_initialize :article

def published_at(view)
view.content_tag(:time, article.published_at.strftime("%A, %B %e"))
end
end

And you can of course add whatever convenience methods you like, by inheritance or otherwise.

The model is available to the presenter as article rather than by a generic name like model or object.

You no longer have that complex, black magic dependency.

Forms and plugins just use regular ActiveRecord objects.

When you use a presenter, that's made perfectly clear. If it makes sense to do some limited delegation, that's just a delegate :title, to: :article away.

It's obvious where the view context comes from. If you use it a lot, feel free to pass it into the initializer instead of into each method (e.g. ArticlePresenter.new(@article, view_context) in the controller).

I can't see why anyone would prefer Draper to this, but I'm looking forward to discussion in the comments.