I’d been looking around for how to do Dependency Injection (Inversion of Control) in my Rails App. I don’t need anything particularly fancy, just the ability for classes (controllers in particular) to have different service implementations injected into them depending on the environment that they’re in. Something which I take for granted in the Java world with Spring’s IoC container.
Now I know that Ruby is a vastly different language than Java and that means that somethings that make sense in the Java world (i.e. Dependency Injection) just may not add up at all in the Ruby world. But I still think that a simple IoC container in my Rails app is required. It helps with unit/mock testing and in particular, it helps when working in an enterprise environment.
I found what appears to be quite a famous article about DI with Rails using Needle here:
http://www.jamisbuck.org/ruby/rails-injected.html
Although it does appear to be thorough (if a little old now), I still felt that I could get what I needed without as much complexity, so this is what I did in my Rails app…
I created a simple service locator module that classes include to access services:
lib/ioc/service_locator.rb
module IOC
module ServiceLocator
def self.included(base)
base.class_eval do
extend ClassMethods
include IOC::ServiceLocator::InstanceMethods
end
end
module ClassMethods
def has_dependency(*names)
names.each {|n| class_variable_set("@@#{n}", Registry.instance.get_service(n)) }
end
end
end
end
A registry for all the services to be cataloged in:
lib/ioc/registry.rb
module IOC
class Registry
include Singleton
def initialize
@registry = {}
end
def self.register(name, instance)
self.instance.register(name, instance)
end
def register(name, instance)
@registry[name] = instance
end
def self.register_if_not_defined(name, instance)
self.instance.register_if_not_defined(name, instance)
end
def register_if_not_defined(name, instance)
register(name, instance) unless has_service(name)
end
def self.get_service(name)
self.instance.get_service(name)
end
def get_service(name)
@registry[name]
end
def self.has_service(name)
self.instance.has_service(name)
end
def has_service(name)
!@registry[name].nil?
end
end
end
Now my controllers can include the ServiceLocator module and nominate dependencies which can then be accessed as class variables, in this case the
workflow_service
class WorktrackerController < ApplicationController
include IOC::ServiceLocator
has_dependency :workflow_service
def wip
@wip = @@workflow_service.get_work_in_progress
end
end
Wiring this up, I add the base registry components at the bottom of:
config/environment.rb
... # Default IOC configuration - overridden by environments # Each environment needs to call the config.to_prepare first for the # Dispatcher class to be loaded! Dispatcher.callbacks[:prepare].insert(0, lambda do IOC::Registry.register(:workflow_service, Service::FakeWorkflowService.new) end)
The registry’s entries can then be overridden by the particular environment, ie:
config/environment/development.rb
...
config.to_prepare do
IOC::Registry.register(:workflow_service,
Service::WebserviceWorkflowService.new('http://tiger:8888/proxy/service/WorkflowInquiry'))
end
So there you go, its simple and it works.



DI is a Ruby anti-pattern. If you read this (http://weblog.jamisbuck.org/2007/7/29/net-ssh-revisited) Jamis basically says he is glad that Needle never caught on. The comments are helpful.
Yeah I think I agree with you now Chris and thanks for pointing out the link too, makes for interesting reading. Jamis did say that he isn’t actually against DI, he just doesn’t think that you need fully fledged DI containers. Like he says, hash maps make for a good container. I know I could find a more elegant way to do what I was after, most likely through metaprogramming. Interestingly I’ve only had one reason to use DI in my Rails apps and that was for an work project where we have many different environments and therefore several service implementations (i.e. an account service for retrieving account information from the enterprise service bus via SOAP), some implementations are completely faked out. I was really just after a way to be to have my controllers use the service without understanding where it came from and how it was implemented.