Finally – helpers with proper encapsulation, delegation, interfaces and inheritance!
Helpers suck. They’ve always sucked, and they will suck on if we keep them in modules.
ActiveHelper is an attempt to pack helpers into classes. This brings us a few benefits
- inheritance helpers can be derived other helpers
- delegation helpers are no longer mixed into a target- the targets
import
the helper, where the new
methods are delegated to the helper instances - proper encapsulation helpers don’t rely blindly on instance variables – a helper defines its
needs
, the target has to provide readers - interfaces a helper clearly
provides
methods and mightimport
additional helpers
Note that ActiveHelper is a generic helper framework. Not coupled to anything like Rails or Merb. Not providing any concrete helpers. Feel free to use clean helpers in any framework (including Rails and friends)!
> gem install active_helper
Let’s use the bloody MVC-View example as we find in Rails or Merb (Sinatra, too?).
We have a view which needs additional methods in order to render bullshit.
The view wants to render tags using the TagHelper.
class View include ActiveHelper end > view = View.new > view.import TagHelper
To pull-in a helper we invoke import
on the target instance.
The exemplary #tag method took me days to implement.
class TagHelper < ActiveHelper::Base provides :tag def tag(name, attributes="") "<#{name} #{attributes}>" end end
The helper defines a part of its interface (what goes out) as it provides
methods.
> view.tag(:form) # => "<form>"
The real power of OOP is inheritance, so why should we throw away that in favor of modules?
class FormHelper < TagHelper provides :form_tag def form_tag(destination) tag(:form, "action=#{destination}") # inherited from TagHelper. end end
That’s a bit cleaner than blindly including 30 helper modules in another helper in another helper, isn’t it?
> view.import FormHelper > view.tag(:form) # => "<form>" > view.form('apotomo.de') # => "<form action=apotomo.de>"
Obviously the view can invoke stuff from the FormHelper itself and inherited methods that were exposed with provides
.
What if the #form_tag method needs to access another helper? In Rails, this would simply be
def form_tag(destination) destination = url_for(destination) tag(:form, "action=#{destination}") end
The #url_for methods comes from, na, do you know it? Me neither! It’s mixed-in somewhere in the depths of the helper modules.
In ActiveHelper this is slightly different.
class FormHelper < TagHelper provides :form_tag uses UrlHelper def form_tag(destination) destination = url_for(destination) # in UrlHelper. tag(:form, "action=#{destination}") end end
Hmm, our FormHelper is already derived from ActiveHelper, how do we import additional methods?
Easy as well, the helper class uses
it.
So we have to know #url_for is located in the UrlHelper and we even have to define which helpers it uses
.
That’s a good thing for a) code tidiness, b) good architecture and c) debugging.
How would the UrlHelper look like?
A traditional url helper would roughly look like this:
def url_for(url) protocol = @https_request? ? 'https' : 'http' "#{protocol}://#{url}" end
Next chance, who or what did create @https_request? and where does it live? That’s ugly, boys!
Our helper bets on declaring its interface, again! This time we define what goes in (a “dependency”).
class UrlHelper < ActiveHelper::Base provides :url_for needs :https_request? def url_for(url) protocol = https_request? ? 'https' : 'http' "#{protocol}://#{url}" end end
It defines what it needs
and that’s all for it. Any call to #https_request? (that’s a method) is strictly delegated back to the view instance, which has to care about satisfying dependencies.
Here’s what happens in productive mode.
> view.form('apotomo.de') # => 11:in `url_for': undefined method `https_request?' for #<View:0xb749d4fc> (NoMethodError)
That’s conclusive, the view is insufficiently geared.
class View include ActiveHelper def https_request?; false; end end
Now, does it work?
> view.form_tag('go.and.use/active_helper') # => <form action=http://go.and.use/active_helper>
Yeah.
Use ActiveHelper in your Rails app! Assuming you’d be writing a helper for text munging, you would
1. Write your helper and put it in app/active_helpers/text_munging_helper.rb
.
class TextMungingHelper < ActiveHelper::Base provides :munge def munge(text) text.rot13 end end
2. Prepare your controller.
class StupidController < ActionController::Base active_helper TextMungingHelper
3. Use the imported methods in your views, just as you know it from other helpers.
<p> Your Email is <%= munge @user.email %>. </p>
- Helpers are instances, when accessing a raw
@ivar
it refers to their own instance variables - Dependencies between different helpers and between the target (e.g. a View instance) are modelled with OOP strategies: Inheritance and the declarative
#needs
.
Copyright © 2010, Nick Sutterer
Released under the MIT License.