Friday, September 12, 2008

Private Mixins - Including helpers in controllers

Despite being fully aware that controllers should include minimal logic, we still have some code that needs to be shared by multiple controllers (and even some views). The easiest way to share code in ruby is, of course, the mixin module. The trouble in rails is that any public methods on controllers are exposed as actions, even ones that come in via mixin. That's trouble. One solution was to define all methods in our helpers as private. Didn't really work in every scenario and kind of limits testing. What we really wanted was to be able to include modules privately. As far as I can tell, ruby doesn't support this off the shelf, so we decided we give it a shot to see where it took us.

class Module
 
  [:private, :protected].each do |type|
    eval %{
      def include_#{type}(*prms)
        prms.each do |mod|
          include(mod)
          mod.instance_methods.each do |meth|
            #{type}(meth)
          end
        end
      end
    }
  end
 
end

This creates two methods include_private and include_protected. These guys wrap the normal include method but then take all the instance methods for the included module and privatize or protected-ize them, respectively. Then we added this to ApplicationController:

class ApplicationController < ActionController::Base

  class << self
    alias_method :include_helper, :include_private
  end
 
end

Now we can include helpers in our controllers privately:

class MyController < ApplicationController
  include_helper MyHelper
end

Now, it seems to me that there must be a better way of doing this and we would love to see it because our googling came up empty and it seems odd that we had to resort to this, though it seems to fit our needs just fine.

2 comments:

technicalpickles said...

I've taken to making submodules that have the private/protected methods, and then doing a class_eval, with the appropriate private/protected keyword before including the module.

Something like:

def self.included(base)
base.class_eval do
include InstanceMethods

protected
include ProtectedInstanceMethods
end
end

sirmigwell said...

Another way which is close to the solution below, is to use Class::private which takes an Array of methods as argument.

def self.included(klass)
klass.class_eval do
private(:method, :another_method)
end
end

The difference is that you don't define a specific module just for your private methods.

So this can be good or bad depending on how you like to have things organized.