2 min read

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.

  • Modules occasionally depended on other modules (there's no point in having Layouts without Renderer), but including Renderer into Layout meant that we couldn't have setup on Renderer that got applied to the controller class itself. In this case, Renderer adds a "_view_paths" inheritable accessor to new Controller classes that is used to store a list of paths containing templates. If we included Renderer into Layouts, and then Layouts into ActionController::Base, that setup would happen on Layouts, which is wrong.
  • We used the def self.included(klass) klass.class_eval { ... } end idiom a whole lot. In fact, that's the only thing we used the included hook for, except...
  • Extending ClassMethods onto the class.
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!