active_helper icon indicating copy to clipboard operation
active_helper copied to clipboard

Finally - helpers with proper encapsulation, delegation, interfaces and inheritance!

h1. ActiveHelper

Finally - helpers with proper encapsulation, delegation, interfaces and inheritance!

h2. Introduction

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 might @import@ 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)!

h2. Installation

> gem install active_helper

h2. Example

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.

h3. Using helpers

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.

h3. Interfaces

The exemplary #tag method took me days to implement.

class TagHelper "
  end
end

The helper defines a part of its interface (what goes out) as it @provides@ methods.

> view.tag(:form)       # => "
"

h3. Inheritance

The real power of OOP is inheritance, so why should we throw away that in favor of modules?

class FormHelper 

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)               # => "
" > view.form('apotomo.de') # => "
"

Obviously the view can invoke stuff from the FormHelper itself and inherited methods that were exposed with @provides@.

h3. Delegation as Multiple Inheritance

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 

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?

h3. Delegation as Interface

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 

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 #<0xb749d4fc>