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…

8 responses

  1. This is wonderful news. Too bad it came a couple days after I figured it out.

    has_many_polymorphs is a wonderful addition that I’m truly surprised isn’t already in core. Just wanted to say thank you for putting this tool out to the world.

  2. Yeah, though your init.rb covers that, you’re just adding a Dispatcher.to_prepare to handle reloading in dev… but as you say, still doesn’t help when you’ve done a reload! in console.

    Maybe the best solution would be a proper reload hook for Rails, rather than to_prepare?

    A hack would be to make reload! call to_prepare. Execute this after reload! and your all your plugin patchy goodness is available again.

    Dispatcher.send(
      :preparation_callbacks).each do |_, proc|
        proc.call unless proc.nil?
    end
    
  3. Thanks for a series of great articles. Unfortunately, I was unable to get this to work. I, was playing w/ a plugin module called Rubaidh::TabularForm to generate tabular forms. In dev mode, first iteration it works, but afterwards it fails b/c ApplicationHelper has been reloaded and this module, which is included into ApplicationHelper via an init.rb file send(:include) method, doesn’t get reloaded. I tried to specify the module as reloadable, but that didn’t do it, and using this Dependencies extension and calling it in my app init configuration didn’t do it either.

    Any ideas are appreciated. Thanks.