DiFtw
Dependency Injection For The Win! A small, yet surprisingly powerful, dependency injection library for Ruby.
Why DI in Ruby?
If your only concern is testing, mocks/stubs and webmock
might be all you need. But if you're working on a large project that hooks into all kinds of enterprisey services, and those services aren't always available in dev/testing/staging, a dash of DI might be just the thing.
Features
- DI container w/dead-simple registration
- Inject singletons or factories
- Lazy injection (by default)
- Inject into each of a class's instances, a single instance, a class itself, or a module
- Uses parent-child injectors for max flexibility
- Threadsafe, after registration
Dead-simple registration API
# Create your root injector/container
DI = DiFtw::Injector.new do
singleton :foo do
OpenStruct.new(message: "Everyone will get this same object")
end
factory :bar do
OpenStruct.new(message: "Everyone will get a NEW version of this object")
end
# inject dependencies into your dependency
factory :foobar, [:foo, :bar] do
OpenStruct.new(message: foo.message + bar.message)
end
end
# Or register things out here
DI.singleton :zorp do
OpenStruct.new(message: "Bar")
end
Lazy injection (by default)
class Widget
include DI.inject :foo, :bar
end
widget = Widget.new
# foo isn't actually injected until it's first called
puts widget.foo.message
=> "Foo"
Lazy injection is usually fine. But if it isn't, use inject!
:
class Widget
include DI.inject :foo, :bar
def initialize
inject!
end
end
# foo and bar are immediately injected
widget = Widget.new
Inject into all instances, a single instance, a class, or a module
# Inject :baz into all Widget instance objects
class Widget
include DI.inject :baz
end
puts Widget.new.baz.message
=> 'Baz'
# Inject :baz into one specific instance
x = SomeClass.new
DI.inject_instance x, :baz
puts x.baz.message
=> 'Baz'
# Inject :baz as a class method
class SomeClass
extend DI.inject :baz
end
puts SomeClass.baz.message
=> 'Baz'
# Inject :baz as a module method
module SomeModule
extend DI.inject :baz
end
puts SomeModule.baz.message
=> 'Baz'
Parent-Child injectors
This is maybe the coolest part. Each time you call inject
(or inject_instance
) you're creating a fresh, empty child DiFtw::Injector
. It will recursively look up dependencies through the parent chain until it finds the nearest registration of that dependency.
This means you can re-register a dependency on a child injector, and it will be injected instead of whatever is registered above it in the chain.
# Create your root injector and register :foo
DI = DiFtw::Injector.new
DI.singleton(:foo) { 'Foo' }
class Widget
include DI.inject :foo
end
class Spline
include DI.inject :foo
end
# Widget and Spline each get a new injector instance
Widget.injector.object_id != DI.object_id
=> true
Widget.injector.object_id != Spline.injector.object_id
=> true
# Each Widget instance gets a new injector instance. Same for Spline.
w1 = Widget.new
w1.injector.object_id != Widget.injector.object_id
=> true
w2 = Widget.new
w1.injector.object_id != w2.injector.object_id
=> true
# But all those child injectors are empty. They'll all resolve :foo
# to whatever is in DI[:foo]
Widget.new.foo
=> 'Foo'
Spline.new.foo
=> 'Foo'
Widget.new.foo.object_id == Spline.new.foo.object_id
=> true
# But we could re-register/override :foo in Spline.injector, and all new
# Spline instances would resolve :foo differently.
Spline.injector.singleton(:foo) { 'Bar' }
Spline.new.foo
=> 'Bar'
# But DI and Widget.injector would be unchanged
Widget.new.foo
=> 'Foo'
# We can go even further and override :foo in just one specific instance of Spline
# NOTE This only works if you're using lazy injection (the default) AND if you haven't called #foo yet
s = Spline.new
s.injector.singleton(:foo) { 'Baz' }
s.foo
=> 'Baz'
# Other Spline instances will still get their override from Spline.injector
Spline.new.foo
=> 'Bar'
# While Widget instances will all still get the original value from DI
Widget.new.foo
=> 'Foo'
DI in testing/local dev/staging/etc.
To inject different dependencies in these environments, you have several options. You can simply re-register dependencies in your root injector:
DI.singleton(:foo) { OpenStruct.new(message: 'Test Foo') }
And/Or you can use the parent-child injector features described above to great effect:
before :each do
# Give all MyService instances 'Test foo' as #foo
MyService.injector.singleton(:foo) {
'Test foo'
}
end
after :each do
# Remove the override & fallback to whatever was registered in the root injector
MyService.injector.delete :foo
end