snax

dependency injection for rails models

The polymorphs plugin dynamically injects methods into child models. This means that if you referenced a child model before the parent was loaded, the methods would be missing.

inversion of control or what have you

I solved the problem by adding a dependency injection mechanism. Here’s the entire code:

module Dependencies
  mattr_accessor :injection_graph
  self.injection_graph = Hash.new([])

  def inject_dependency(target, *requirements)
    target, requirements = target.to_s, requirements.map(&:to_s)
    injection_graph[target] =
      ((injection_graph[target] + requirements).uniq - [target])
    requirements.each {|requirement| mark_for_unload requirement }
  end

  def new_constants_in_with_injection(*descs, &block)
    returning(new_constants_in_without_injection(*descs, &block)) do |found|
      found.each do |constant|
        injection_graph[constant].each {|req| req.constantize}
      end
    end
  end
  alias_method_chain :new_constants_in, :injection
end

explanation, usage

See what it does? Imagine that the Tag and Tagging classes modify the Recipe class (by injecting a new method, or relationship, or something). Normally in development mode if Recipe gets reloaded, the injections will get lost, since Rails doesn’t know it has to re-evaluate Tag and Tagging after Recipes is refreshed. But now we can do as follows:

Dependencies.inject_dependency("Recipe", "Tag", "Tagging")

This way, when the Recipe constant gets refreshed, Tag and Tagging will also get refreshed, and can go patch up Recipe again. Because constantize() doesn’t reload the same constant multiple times, there is no danger of infinite cycles.

remember, only for development mode

This is not at all useful in production mode, since classes aren’t reloaded. But in development mode it can make a big difference in sanity.

If there is interest I can release it as a separate plugin.

postscript: on the plugin boot process

Also in version 27.1, there is some method chaining to let the plugin finish booting itself after the config.after_initialize block runs. This is useful because users are supposed to set plugin start-up options in config.after_initialize (see here).

The startup sequence is like so (compressed from railties/lib/initializer.rb):

def process
    load_environment  # environment.rb
    load_plugins # init.rb for your plugin
    load_observers
    initialize_routing
    after_initialize # where your user configures your plugin
end

What if things in init.rb need to know the configuration options? Check lib/has_many_polymorphs/autoload.rb for an example of a fix.

Unfortunately config.after_initialize doesn’t allow multiple blocks the way Dispatcher.to_prepare does. There is a Rails patch waiting to happen here…