Skip to content

Instantly share code, notes, and snippets.

@namtx
Forked from somebox/presenters.md
Created June 15, 2017 08:26
Show Gist options
  • Save namtx/fb14232f404bdfc4c9cfd95bc5093262 to your computer and use it in GitHub Desktop.
Save namtx/fb14232f404bdfc4c9cfd95bc5093262 to your computer and use it in GitHub Desktop.
Thoughts About Rails Presenters

Thoughts about Rails Presenters

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


Presenters are supposed to solve...

  • too much code in views
  • bloated helpers
  • models with view code

Basic Idea

  • Move business logic from controller and views
  • Separation of concerns
  • Law of Demeter
  • Easier to refactor views

Experience

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

Helper

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

In the View

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

Fuuuuuuuuuuuuuuuu....

def method_missing(*args, &block)
  self.send(:look_im_clever) || yield
  self.bang_head_against_wall! rescue nil
  return :to_basics
end

Problems We Had

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.


A Friggin' Question

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?


Enter Draper

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.


Dear Draper

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.


Plain old Ruby Objects (PORO)

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/


Helpers can be Helpful

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.


Helpers are often Unhelpful

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: Conclusion

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.


Helpers: Best Practice?

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: Conclusion

Helpers should be helpful. Delete unhelpful helpers!


Partials

  • 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.

Partials are Awesome

- @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.


Lesson Learned

  • 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

YAGNI

  • think of better abstractions?
  • consider different naming?
  • maybe you just need a new model?

The Big Picture

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

Helpers and Partials Together in Harmony

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/


References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment