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.