27 March 2012

Real Views with Draper

I recently latched on to a tweet thread that was originated by Michael Guterl (@mguterl), in which he expressed his frustration with view helpers and erb in rails. The frustration was echoed by others, and being somewhat new to Rails, I can’t say that I’ve personally felt much pain first hand, but’s my impression from the the get-go, is that erb templates seem to get ugly quickly, and helpers aren’t properly object oriented.

On the first count, I can’t much fault erb for getting ugly. Those little are powerful, you can’t really blame the tool for providing you all the rope you need to hang yourself with. I’m kind of intrigued by the idea of logic-less templates though, in a masochistic kind of way, but maybe that’s an investigation for another blog post.

On the lack of object orientation, I’m intrigued. This seems wrong, but what is the pain point here exactly?

During the course of the thread, Chris Nelson (@superchris) mentioned he had been working with the Draper gem on a project and it had so far seemed like a win. The idea of Draper is to provide real ruby class between the template and the model where presentation concerns can reside. A view class, rather than a helper. I decided to give it a try.

My starting point was to seek out an erb template on a production project that was getting particularly ugly with logic, and see if Draper could help me clean it up. I wanted to see how much I could do with helpers, and then if an OO view could improve upon that.

Getting Started

I watched the Draper RailsCast. Installing the gem, I got a generator that I could feed an existing model class. It generated a decorator class associated to that model in a new app/decorators/ directory. The class looks like this.

class CallReportSummaryDecorator < ApplicationDecorator
   decorates :call_report_summary

The Decorator is a design pattern that “attach(es) additional responsibilities to an object dynamically”. Wrapper, or Presenter is another term. Our Draper decorator is a view. It adds a layer of view related code, and delegates everything else to the model it wraps.

I instantiate the new class in the controller, that here used to return a collection of CallReportSummary objects. CallReportSummary(s) calculate and compile some metrics, and the index page displays a table of these metrics with a row for each summary.

def index
  @current_tab = "Statements"
  @summaries =
    @users.collect { |user|
      CallReportSummaryDecorator.new( CallReportSummary.new(user, @date_range))}
end

The controller now returns a collection of those objects wrapped in decorators, but the app behaves as it always has. That’s because the decorator is automatically delegating all calls to the established model through method missing. This is kind of an interesting divergence from the gang of four implementation of the decorator, which get’s it’s methods by subclassing the object it decorates.

Usage

So how do we use it?

Consider the following piece of code that renders user data from a report summary object into a table field:

<td><%= summary.display_name %><%= ghost_icon(summary.user) %></td>

With a helper we could clean it up a little like:

<td><%= display_user(summary) %></td>

But that’s where things get screwy with helpers. Maybe a functional programmer can set me straight on this someday, but it seems more suitable for display_user to be method of the summary, as opposed to a function that acts on it.

<td><%= summary.display_user %></td>

Regardless of your preference there, the fact remains that there’s no intuitive way to structure gobs of helper code, in the way that standard Ruby OOP can provide.

So, if you don’t like the helper, your next option is to put the display_user method on the model, where it doesn’t belong. Views of a model are different than the model, and you should have the capacity to view a model in different ways. Draper provides that capacity where Rails doesn’t. With decorators, you can leave the model alone, and implement display_user in a view that wraps the model. Then, you can turn around and implement display_user again as many different times, in as many different views.

The Draper decorator has access to the model it decorates through @model, and to all helper functions through the h. proxy. That includes ones you’ve defined, and standard Rails ones like link_to and content_tag.

So the implementation in the decorator just becomes:

def display_user
  @model.display_name + h.ghost_icon( @model.user )
end

I guess the lesson I learned here is that Draper, to me, is not so much about cleaning up templates. That can be done with helpers and partials. So I decided to investigate more the use of Draper as a tool for good OO design.

Good Design

That user data rendering code was taken from this view that renders a table with a row for each report summary returned by the controller.

<table>
  <thead>
    <tr>
      <th>User</th>
      <th>Clicks</th>
      <th>Calls</th>
      <th>Convs</th>
      <th>Cost</th>
      <th>Cost/Click</th>
      <th>Cost/Call</th>
      <th>Click-To-Call</th>
      <th><input type="checkbox" name="check_all" id="check_all" value="all"/></th>
    </tr>
  </thead>
  <tbody>
    <% @summaries.each do |summary| %>
    <tr>
      <td><%= summary.display_name %><%= ghost_icon(summary.user) %></td>
      <td><%= format_number(summary.total_clicks) %></td>
      <td><%= format_number(summary.total_calls) %></td>
      <td><%= format_number(summary.total_conversions) %></td>
      <td><%= format_microcents(summary.total_customer_cost) %></td>
      <td><%= format_microcents(summary.average_customer_cpc)  %></td>
      <td><%= format_microcents(summary.cost_per_call)  %></td>
      <td><%= format_percentage(summary.calls_per_click)  %></td>
      <td><input type="checkbox" name=send_summary[] id=send-<%= summary.id %> value="<%= summary.id %>" class="send_summary" /></td>
    </tr>
  <% end %>
  </tbody>
</table>

Each report row contains a checkbox that when checked and submitted, will render that single report summary object into an email statement and send it off to that particular client. The data rendered in that email statement is different from the data rendered in the above administrator’s view:

<table>
  <thead>
    <tr>
      <th>Total Amount</th>
      <th>Impressions</th>
      <th>Clicks</th>
      <th>Cost/Click</th>              
      <th>Calls</th>
      <th>Convs</th>
      <th>Cost/Contact</th>
      <th>Click-to-Contacts</th>
      <th>Average Position</th>
    </tr>
    <tr>
      <td><%= format_microcents(@summary.total_customer_cost) %></td>
      <td><%= number_with_delimiter(@summary.total_impressions.to_i) %></td>
      <td><%= format_number(@summary.total_clicks) %></td>
      <td><%= format_microcents(@summary.cost_per_click) %></td>
      <td><%= format_number(@summary.total_calls) %></td>
      <td><%= format_number(@summary.total_conversions) %></td>
      <td><%= format_microcents(@summary.cost_per_call) %></td>
      <td><%= format_percentage(@summary.calls_per_click) %></td>
      <td><%= number_with_precision(@summary.average_pos, :precision => 2) %></td>
    </tr>
  </thead>
</table>

Hence, a scenario in which a particular model is displayed in two different ways. The driving idea here is that the template should not also be the view. The template should be concerned primarily with styling and markup, not with choosing the data to be displayed. We have a view decorator now that can determine that. With that in mind, my Draper-fied tables became:

<table>
  <thead>
    <tr>
     <th>User</th>
      <% table_view.table_headers.each do | table_header | %>
        <th><%= table_header %></th>
      <% end %>
     <th><input type="checkbox" name="check_all" id="check_all" value="all"/></th>
    </tr>
  </thead>
  <tbody>
  <% @summaries.each do |summary| %>
    <tr>
      <td><%= summary.display_user %></td>
      <% summary.attributes.each do |attribute| %>
        <td><%= summary.render_attribute(attribute) %></td>
      <% end %>
      <td><input type="checkbox" name=send_summary[] id=send-<%= summary.id %> value="<%= summary.id %>" class="send_summary" /></td>
    </tr>
    <% end %>
  </tbody>
</table>

And the email:

<table>
  <thead>
    <tr>
    <% table_view.table_headers.each do | table_header | %>
      <th><%= table_header %></th>
    <% end %>
    </tr>
    <tr>
    <% summary.attributes.each do |attribute| %>
      <td><%= summary.render_attribute(attribute) %></td>
    <% end %>
    </tr>
 </thead>

Note that the replacement looping code is identical between the two views. The difference is in how table_headers and attributes are defined in the Draper decorators that wrap the summary before it’s either 1) displayed to an admin screen or 2) rendered into an client email.

Previously, our decorator was called CallReportSummaryDecorator, the name provided by the decorator generator. My inclination, now that we have two wrappers decorating the same model, is to rename it more appropriately to what it is, i.e. CallReportSummaryAppTableView. And then, to have a corresponding CallReportSummaryEmailTableView.

Since each table header matches up with a piece of data, to me it makes sense to define a hash in each decorator that provides the configuration for each table.

In CallReportSummaryAppTableView:

def column_attribute_map
  {
    "Clicks" => :total_clicks,
    "Calls" => :total_calls,
    "Convs" => :total_conversions,
    "Cost" => :total_customer_cost,
    "Cost/Click" => :average_customer_cpc,
    "Cost/Call" => :cost_per_call,
    "Click-To-Call" => :calls_per_click,
  }
end

In CallReportSummaryEmailTableView:

def column_attribute_map
  {
    "Total Amount" => :total_customer_cost,
    "Impressions" => :total_impressions,
    "Clicks" => :total_clicks,
    "Cost/Click" => :cost_per_click,
    "Calls" => :total_calls,
    "Cost/Call" => :cost_per_call,
    "Click-to-Contacts" => :calls_per_click,
    "Average Position" => :average_pos,
  }
end

table_headers and attributes are methods that access that configuration:

def table_headers
  column_attribute_map.keys
end

def attributes
  column_attribute_map.values
end

The identical implementations of those two methods in the two view classes calls for a refactoring, a superclass, so I create a CallReportSummaryTableView. Those methods can be defined once in that class and inherited. render_attribute is in the same boat, it’s implementation is:

def render_attribute(symbol)
  send(symbol)
end

Formatted attributes that are shared between the two views, like :calls_per_click, can be implemented in the superclass view like:

def calls_per_click
  h.format_percentage(@model.calls_per_click)
end

Formatted attributes specific to each leaf view class can implemented there.

Note that the Draper gem generates an ApplicationDecorator class where general formatting methods, like format_percentage, can be defined. Using it, it’s likely you could bypass application-specific helpers entirely.

def calls_per_click
  format_percentage(@model.calls_per_click)
end

Conclusion

I don’t know if I performed any mind blowing feats here, but I feel good about this. It’s a bit cleaner, and my view code, previously in helpers, has a nice OO home now where I’ve already started to do some refactoring and code- sharing. In the template, I’m able to call display-related methods on the model, which feels more natural than invoking a detached helper method. But I don’t have to implement those display concerns in the model itself, where they don’t belong.

All in all, I’m feeling confident that Draper is a crucial ingredient to good design. This feels less like a gem that provides some functionality, and more like an integral, missing piece of Rails to me.

Heads up! This article may make reference to the Gaslight team—that's still us! We go by Launch Scout now, this article was just written before we re-introduced ourselves. Find out more here.

Related Posts

Want to learn more about the work we do?

Explore our work

Ready to start your software journey with us?

Contact Us