Better Module Organization

As I said in my last post, Carl and I have been working on a more modular version of ActionController. As we fleshed out the feature set, we had a few needs that aren't directly addressed by Ruby's built-in feature-set.

When I was at Locos X Rails, I spent some time with Evan, and he argued that using the included hook should be done only after trying other abstractions. After speaking for a few minutes, Evan suggested abstracting away the above ideas in a higher-level abstraction that wrapped include. We could then more directly control the inclusion process, and even add our own hooks where needed.

I ended up with:

module AbstractController
  module Callbacks
    setup do
      include ActiveSupport::NewCallbacks
      define_callbacks :process_action
    end
    ...
  end
end

replacing:

module AbstractController
  module Callbacks
    def self.included(klass)
      klass.class_eval do
        include ActiveSupport::NewCallbacks
        define_callbacks :process_action
        extend ClassMethods
      end
    end
    ...
  end
end

For dependencies, I replaced:

module AbstractController
  module Helpers
  
    def self.included(klass)
      klass.class_eval do
        extend ClassMethods
        unless self < ::AbstractController::Renderer
          raise "You need to include AbstractController::Renderer before including " \
                "AbstractController::Helpers"
        end
        extlib_inheritable_accessor :master_helper_module
        self.master_helper_module = Module.new
      end
    end
    ...
  end
end

with

module AbstractController
  module Helpers
    depends_on Renderer
    
    setup do
      extlib_inheritable_accessor :master_helper_module
      self.master_helper_module = Module.new
    end
    ...
  end
end

And finally, the Base controller itself could now be replaced with:

module ActionController
  class Base2 < AbstractBase
    use AbstractController::Callbacks
    use AbstractController::Helpers
    use AbstractController::Logger
 
    use ActionController::HideActions
    use ActionController::UrlFor
    use ActionController::Renderer # just for clarity -- not required
    use ActionController::Layouts
  end
end

from:

module ActionController
  class Base2 < AbstractBase
    include AbstractController::Callbacks
    include AbstractController::Renderer
    include AbstractController::Helpers
    include AbstractController::Layouts
    include AbstractController::Logger
    
    include ActionController::HideActions
    include ActionController::UrlFor
    include ActionController::Layouts
    include ActionController::Renderer
  end
end

It's not a tremendous change, but it definitely reduces the likelihood of accidental mistakes, and makes the actual usage a lot clearer. Of course, we will need to document this new mechanism, but it has already simplified the necessary mental model of the setup.

As always, thanks for reading!