Monday, May 19, 2008

Declarative Class Method Prettiness for ActionHelper

I don't know why it didn't occur to me sooner, but the most Rails-consistent way for ActionHelper to support a controller declaring action-specific helper modules is pretty obviously extending the existing ActionController::Helpers#helper method to accept an options hash supporting :only and :except, the same way the filter class methods do.

It may not be the coolest way to do it, but it's definitely the least surprising.

So ActionHelper now supports :only and will likely support :except sometime soon. (Though except seems much less likely to be useful.)

Wednesday, May 14, 2008

In defense of helpers

...in which I defend helpers[1] as good OO, if you use them just so; point out an aspect of the convention that stands in the way of that style; and provide an alpha plugin that tries to change that.

By Rails convention helper modules are where view logic belongs. During request processing, a controller will automatically look for a module in the helpers directory with a name matching the controller. If found, the module will be mixed in to the controller's response's template object, an instance of ActionView::Base (and self in the rendered erb template). A controller can also specify additional helper modules to mix in using the helper class method.

The typical approach I've seen is to define helper methods that take model objects or their attributes as arguments (where the model was typically put into an instance variable by the controller). So the template does something like

<span class='contributors'>
  <%= contributors_list @project.contributors %>
</span>

to use a helper like this

module ProjectsHelper
  
  TOO_MANY = 10
  
  def contributors_list contributors
    if contributors.size < TOO_MANY
      contributors.to_sentence
    else
      contributors[0...(TOO_MANY - 1)].join(', ') + ', and more'
    end
  end
end

I think it's because of this functional style of helper method that I've seen a fair amount of bias against helpers. OO developers like encapsulation, and helper modules generally encapsulate logic but not the information needed to apply that logic.

For example, the first Rails project I was on didn't use any application helpers. The team had created a parallel construct called presenters. The "final" state of the presenter stuff evolved over months of development, but by the end, a page-specific presenter object was always made available in the @presenter instance variable (thanks to some frameworkey extensions in our ApplicationController based on a naming convention), and eventually a method_missing was monkey-patched into ActionView::Base to automatically delegate everything to @presenter so our templates weren't cluttered.

By the time the method_missing went in, we'd come back around to something very close to Rails' built in helpers, and I had a little bit of an aha moment. The helper is the page (because it's mixed in), I thought, why would I pass it my instance variables?

The approach of taking in arguments for things that could have been pulled from instance variables is consistent with a general rule in Ruby that modules ought not to mess with instance variables if they can avoid it. This rule makes good sense in general-purpose modules (like Enumerable or Comparable) because by design these modules are meant to be mixed in to all sorts of objects, and they don't want to put weird constraints on their hosts. (Imagine if the documentation for Enumerable told you "in addition to providing an #each method, the object should take care to avoid using instance variables called @_cursor, @_enumerators, @...." No one would like that.)

Helper modules aren't like that though. They're designed for a specific page or set of pages (i.e., a specific view concern) in your application. The only reason they're modules rather than classes is that you might have multiple view concerns on the same page.

So I thought it might be interesting to let the helper know more and the template know less about Ruby by moving knowledge of the controller-exposed instance variables into the helper. It worked and felt good.

For a while.

Then I realized that the helper wasn't a page definition: it was a few of them. Since all the actions on a controller get the controller's helpers mixed in, a typical controller would have listing pages, detail pages, and edit pages all with the same helper modules.

Blast!

So I wanted helpers to be selected per action rather than controller-wide. But they weren't. So after talking about it for a while, last Friday morning I finally rolled a Rails plugin to make it the way I wanted: it's called ActionHelper and you can find it on GitHub.

For the moment, it does the naming convention thing that you'd probably expect: when processing UsersController's show action, the module UsersShowHelper will be mixed in to the template if it exists. It also allows actions to declare what helper modules they want by calling action_helper inside the action. (You'd expect a class method, and I agree there should be one, but I haven't figured out a pretty API yet, so for now it's not there.) See the README for an actual example.

If you have thoughts on a pretty declarative class method API for this (whether it's annotation-style or more Railsey), call it out in the comments. Better yet, fork the repository on GitHub and send me a pull request once you've got something going. (ActionHelper has been my "get comfy with git" mini-project.)

Thanks for reading.


1^ Note that I'm talking about the helpers in your application, not the ones Rails provides in ActionView::Helpers. Those are general-purpose modules.