This is a collection of links, examples and rants about Presenters/Decorators in Rails.
The "Decorator" pattern slowly started gaining popularity in Rails several years ago. It is not part of core Rails, and there's many different interpretations about how it should work in practice.
Jay Fields wrote about it in 2007 (before he switched back to Java and then Clojure): http://blog.jayfields.com/2007/03/rails-presenter-pattern.html
Ryan Bates of RailsCasts had this episode (2011): http://railscasts.com/episodes/287-presenters-from-scratch
- too much code in views
- bloated helpers
- models with view code
- Move business logic from controller and views
- Separation of concerns
- Law of Demeter
- Easier to refactor views
On our large-ish project, we embraced presenters a few years ago. This was the basic approach we took to start with:
# Similar approach to Draper (decorator) but more flexible,
# based on solution shown on episode 287 of RailsCasts (Pro):
# SRC http://railscasts.com/episodes/287-presenters-from-scratch
class BasePresenter
def initialize(object, template)
@object = object
@template = template
end
def object
@object
end
private
def self.presents(name)
define_method(name) do
@object
end
end
def h
@template
end
# this allows any template methods to be called directly from presenter code.
def method_missing(*args, &block)
# TODO check for @template.respond_to? and return raw values if nil
@template.send(*args, &block)
end
end
We had a helper method to instantiate the presenters:
def present(object, klass=nil)
klass ||= "#{object.class}Presenter".constantize
presenter = klass.new(object, self)
yield presenter
end
The view code (from our actual app) ended up being rather jumbled. Notice we need two helpers and we still access the original model:
- presenter = Local::ListingPresenter.new(@listing, self)
- content_for(:meta_description) do
= description_for_detail_entry(presenter)
- id = "listing_#{presenter.entity_id}"
.details{id:id}
.marker{data: marker_data(@listing)}
- @listing.entries.each_with_index do |entry, i|
- present(entry, Local::EntryPresenter) do |entry_presenter|
- if entry_presenter.organization_unit_line.present?
%p.organization= entry_presenter.organization_unit_line
def method_missing(*args, &block)
self.send(:look_im_clever) || yield
self.bang_head_against_wall! rescue nil
return :to_basics
end
Presenters, when misused, can make your life hell.
Cleaning up view code does not mean hiding it in another folder.
Presenters can make refactoring harder for complex views.
Why is it that the concept of Presenters/Decorators have never made it into Rails core? Despite the fact that things like Concerns have.
When reading about presenters, often the examples used do not sell the pattern so well. Maybe there's a reason for that?
Draper seems to solve this problem better than most do. But it's still very magical. Please read: http://thepugautomatic.com/2014/03/draper/
from the docs:
"Decorators are supposed to behave very much like the models they decorate, and for that reason it is very tempting to just decorate your objects at the start of your controller action and then use the decorators throughout. Don't."
If you find that you need to instantiate presenters all the time, you've become dependent on them.
However. Draper and other decorators enhance a model. I think presenters are better as separate objects, not decorating the model.
Why? Because with decoration, you end up with two possible cases in your app where a model could be decorated or not. There's also weird issues that can happen with pagination, delegation, method name conflicts with mixins, etc.
So I have concluded that presenters and models should stay as totally different concepts.
Present it, don't decorate it.
There's hope here.
http://joncanady.com/blog/2012/01/11/presenters-cleaning-up-rails-views/
Use plain ruby classes to create models for presenting things.
Here's another clean example: http://www.inspire.nl/blog/rails-presenters-filling-the-model-view-controller-gap/
View helpers have access to other helper methods.
A quick and easy way to move code out of the views.
In general, they are like global utility methods.
Helper which accept blocks can be really handy. You can even capture the view code in a block and wrap or change it.
When debugging view code (particularly partials), it can be impossible to tell what's a helper method and what is an instance var, something provided by a gem, or whatever.
Helpers can create a mess in your code.
You can namespace them, but they are still global.
Helpers can be a pain to test.
In a large app, it's easy to miss a helper that is already in use.
Helpers are like extending Rails for your app. They feel like part of Rails itself.
Try to maintain a very small collection of helper methods.
Helpers should be well-understood, documented, and used consistently.
Avoid writing helpers that accept too many values or return multiple values.
Only use helpers when the concepts are view-related and affect presentation across the app. Avoid helpers that are specifically tied to a single model or single use case.
Developers need to know the helper landscape of an app.
In some cases, a helper may need to be refactorered, but it's hard to find how and where it is used.
Try not to create helpers that call other helpers, which leads to dependency hell.
Helpers should be helpful. Delete unhelpful helpers!
- Partials exist in Rails to help you organize view code.
- Views are not supposed to contain business logic.
- We can often refactor complex views to partials, which can be powerful.
- @presenter = ContactPresenter::PhoneNumber.new(@listing.contact)
= render @presenter
If set up and implemented correctly, it can render _contact.html.haml
in the current folder with the current view hierarchy.
- Presenters (or Decorators) should represent a strong concept.
- Presenters are essentially view models
- PORO (Plain Old Ruby Objects) and well-considered naming are better than decorators
- Ruby objects are easier to test in isolation
- Maintain well-organized helpers
- think of better abstractions?
- consider different naming?
- maybe you just need a new model?
Rails gives us Models, Helpers, and Partials, and if we have the right concepts and abstractions, they work really well.
# controller
@user = User.find(params[:id])
# Helper
def profile_for(@user)
yield ProfilePresenter.new(@user)
end
# View
= render profile_for(@user) # /app/views/user/_profile.html.haml
By using presenters that can be rendered, and helpers that can (but don't have to) use blocks, nice patterns emerge.
See more: http://blog.nhocki.com/2012/05/08/mixing-presenters-and-helpers/
- shakacode/fat-code-refactoring-techniques#11
- http://www.slideshare.net/justingordon/rails-conf-2014concernsdecoratorspresentersserviceobjectshelpershelpmedecideapril222014
- http://railscasts.com/episodes/287-presenters-from-scratch
- https://github.com/drapergem/draper
- http://blog.jayfields.com/2007/03/rails-presenter-pattern.html
- http://thepugautomatic.com/2014/03/draper/
- http://blog.nhocki.com/2012/05/08/mixing-presenters-and-helpers/