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
to use a helper like this
TOO_MANY = 10
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.
4 comments:
How do you test your helpers? If the methods rely on instance variables, do you mix the Helper into your test class?
That was Rspec's default behavior prior to their recent release, so yes, that's what I've done.
I wasn't crazy about the idea at first, but it was the convention so I followed it. I came around to appreciating the feature though. It made sense because it meant you made state available to the helper being specified the same way in your specs that you would in the "real world." That is, you set instance variables in your controller and those magically become available in your view. Your specs could do the same thing.
Now that Rspec has changed the convention (to exposing a helper method that gives you an instance of ActionView::Base with your helper mixed in), I'm not sure what the cleanest way is to get instance variables in there. If I weren't on vacation and late for breakfast right now, I'd try to figure it out immediately, but I promise to figure it out pretty soon and post something.
I should have linked to this: http://blog.davidchelimsky.net/articles/2008/05/29/rspec-waving-bye-bye-to-implicit-module-inclusion
I realize this doesn't count as "pretty soon" but better late than never.
In addition to the #helper method that returns an instance of ActionView::Base that mixes in your helper, it also exposes an #assigns method you can use to set instance variables into the helper.
See http://rspec.info/documentation/rails/writing/helpers.html.
Post a Comment