stderr

secondary output stream of a software developer

Nested Helper Classes vs Private Helper Methods

While criticism towards Rails helpers has almost become folklore (procedural, global scope, junk drawers), I still find myself reaching for them as a low ceremony way of making templates more concise and building simple view layer APIs.

Obviously, the greatest downside of helper modules is how dangerous extracting private methods becomes.

module UsersHelper
  def user_section(user)
    content_tag(:section, user.name, class: css_class(user))
  end

  private

  def css_class(user)
    ['user', user.role].join(' ')
  end
end

module PostsHelper
  def post_section
    content_tag(:section, post.text, class: css_class(post))
  end

  private

  # collides with UsersHelper#css_class
  def css_class(post)
    'post'
  end
end

While I always liked extracting classes to keep helper methods short, only recently did I realize how a nuance in Ruby's constant look-up mechanism helps prevent namespace collisions as above.

Let's look at a variant of the above helpers that uses nested classes instead of private methods.

module UsersHelper
  def user_section(user)
    Section.new(user).render(self)
  end

  class Section < Struct.new(:user)
    def render(template)
      template.content_tag(:section, user.name, class: css_class)
    end

    private

    def css_class
      ['user', user.role].join(' ')
    end
  end
end

module PostsHelper
  def post_section(post)
    Section.new(post).render(self)
  end

  class Section < Struct.new(:post)
    def render(template)
      template.content_tag(:section, post.text, class: css_class)
    end

    private

    def css_class
      'post'
    end
  end
end

At first glance, one could expect the same problem as above. Instead of both defining an equally named private method, both modules now contain a nested class with the same name. Once both modules get included into the view object only one can win, right? That's sure the way it looks when we try to reference one of the nested classes from a template:

  # app/views/users/index.html.erb
  <%= Section == PostsHelper::Section # => true %>

But in contrast to method invocations, which are resolved by the object handling the call, constant look-up uses the lexical scope at the point where the constant is referenced. So referring to a Section class inside UsersHelper still resolves to UsersHelper::Section, no matter whether there is a colliding PostsHelper::Section class when including helper modules into the view context.

Nested helper classes thus provide a safe space for refactoring towards small methods. Helper methods end up containing only wiring code, keeping implementation details from leaking into tests or templates.

comments powered by Disqus