I Take it Back! Just Use PORO.

In my last post I suggested using draper for you decorator needs, I've recently come to believe that advice is only half-right. There's really no valid reason to use draper in most use cases, if you can show me one I'd love to hear it.

What Does Draper Offer That POROs Don't?

IMO, within most generic apps, nothing! You can implement all of the core features of Draper very easily using plain ruby without all of the bloat or context manipulation.

Here's an example from the previous blog post

# app/decorators/zombie_decorator.rb

class ZombieDecorator < Draper::Decorator  
  def health_status
    if object.is_decapitated?
      "Killed at #{decapitated_at}"
    else
      "Undead"
    end
  end


  def decapitated_at
    object.decapitated_at.strftime("%A, %B %e")
  end
end  

Here's how that looks in ruby

# app/presenters/zombie_presenter.rb

class ZombiePresenter  
  def initialize(zombie)
    @zombie = zombie
  end

  def health_status
    if zombie.is_decapitated?
      "Killed at #{decapitated_at}"
    else
      "Undead"
    end
  end

  def decapitated_at
    zombie.decapitated_at.strftime("%A, %B %e")
  end

  attr_reader :zombie
end  

It is a little longer; however, we do not require Draper as a dependency meaning debugging is easier and there is less magic.

Other Issues With Draper

Draper also gently encourages you to make bad decisions. On numerous occasions I've wasted time wondering why @post.published_at or @user.activated_at returns a formatted date or maybe even an html element. It's very easy to overlook the fact that the attribute has been modified in one of your decorators when the object you are working with is named @post or @user.

Draper encourages you to instantiate your objects using their original naming schemes i.e. @book = Book.first.decorate which, not only confuses the namespace; it also makes it difficult to search the codebase for occurrences of the non-decorated vs decorated instances, and makes it impossible to know which object you are looking at when you open a view file without also having to open the controller.

Draper also works some magic to make Rails accept the non-model objects in the forms and links and forces you to delegate calls to the original object for pagination gems such as kaminari. For simple presenter objects, Draper is very bloated, there's really no need for most of these methods in a generic presenter.

How I Create PORO Presenters

In my last example I didn't use the view context, you don't always need to use it; however, many presenters will use the view context to utilize view methods. I like to implement a base_presenter to handle this.

# app/presenters/base_presenter.rb

class BasePresenter  
  def initialize(view)
    view.class_eval { include Haml::Helpers }
    view.init_haml_helpers
    @view = view
  end

  attr_reader :view
end  

Note: If you are using haml then you will want to initialize the haml helpers, otherwise you can just instantiate the view.

Now we have the base class we can use our view helpers from within our presenters by calling super and passing in the view_context object.

# app/presenters/zombie_presenter.rb

class ZombiePresenter < BasePresenter  
  def initialize(zombie, view)
    super view
    @zombie = zombie
  end

  def health_icon
    view.content_tag(:span, health_status, class: 'health_icon')
  end

  def zombie_menu
    view.content_tag(:ul) do
      concat(view.content_tag(:li, health_status))
      concat view.content_tag(:li) do
        if zombie.is_decaptiated?
          view.link_to('Drag to zombie testing lab', view.zombie_lab_path)
        else
          view.link_to('View health stats', view.zombie_health_stats_path)
        end
      end
    end 
  end

  def health_status
    if zombie.is_decapitated?
      "Killed at #{decapitated_at}"
    else
      "Undead"
    end
  end

  def decapitated_at
    zombie.decapitated_at.strftime("%A, %B %e")
  end

  attr_reader :zombie
end  

Now we can instantiate this in the controller as a separate object to the non-modified instance which we can call in the view.

# app/controllers/zombies_controller.rb

class zombies_controller < app_controller.rb  
  def show
    @zombie = Zombie.find(params[:id])
    @zombie_presenter = ZombiePresenter.new(@zombie, view_context)
  end
end  

With this implementation we can easily see which object we are referencing in the view without opening the controller file.
We can search the codebase for occurrences of either object.
We don't need to use magic to force Rails to play nicely with modified instances.

Joe Woodward

Read more posts by this author.