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
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.