Dry::Core::Container is a simple, thread-safe container, intended to be one half of a dependency injection system, possibly in combination with dry-auto_inject. It used to be a separate gem (Dry Container) and it powers containers in Dry System.
Brief Example
container = Dry::Core::Container.new
container.register(:parrot) { puts a }
parrot = container.resolve(:parrot)
parrot.call("Hello World")
# Hello World
# => nil
What are containers and dependency injection?
At its most basic, dependency injection is a simple technique that makes it possible to implement patterns or principles of code design that rely on object composition, such as the SOLID principles. By being passed its dependencies instead of instantiating them itself, your code can be written to depend on abstractions, with implementations that can vary independently, potentially at runtime or for specific use-cases, such as injecting a double instead of an expensive web service call when running tests. A container offers two main improvements to basic dependency injection: it takes the work out of manually instantiating and composing trees of dependencies, and it makes it trivial to swap out one implementation of a dependency for another.
Note that dependency injection, dependency inversion, and inversion of control are related, but distinct, concepts that are often confused or conflated. Inversion of control is an architectural pattern by which a low-level system passes control to higher-level application code, as opposed to the classical pattern, where higher-level code calls directly into a lower-level dependency. Dependency inversion is a principle that encourages thoughtfully designing the interfaces that your classes depend on, instead of tightly coupling to an external dependency's interface. This shouldn't imply that the external dependency itself changes in any way; instead it encourages the use of bridge, facade, or adapter classes to implement the interface that you designed using the third party dependency's public interface. Dependency injection, finally, is the practical technique of providing an object with its dependencies, instead of hard-coding them.
Dry::Core::Container makes it much easier than with so-called "idiomatic" Ruby to make use of any one or all three of these, as desired.
Detailed Example
= Struct.new(:name, :email)
data_store = Concurrent::Map.new.tap do
ds[:users] = Concurrent::Array.new
end
# Initialize container
container = Dry::Core::Container.new
# Register an item with the container to be resolved later
container.register(:data_store, data_store)
container.register(:user_repository, -> { container.resolve(:data_store)[:users] })
# Resolve an item from the container
container.resolve(:user_repository) << User.new('Jack', 'jack@dry-container.com')
# You can also resolve with []
container[:user_repository] << User.new('Jill', 'jill@dry-container.com')
# => [
# #<struct User name="Jack", email="jack@dry-container.com">,
# #<struct User name="Jill", email="jill@dry-container.com">
# ]
# If you wish to register an item that responds to call but don't want it to be
# called when resolved, you can use the options hash
container.register(:proc, -> { :result }, call: false)
container.resolve(:proc)
# => #<Proc:0x007fa75e652c98@(irb):25 (lambda)>
# You can also register using a block
container.register(:item) do
:result
end
container.resolve(:item)
# => :result
container.register(:block, call: false) do
:result
end
container.resolve(:block)
# => #<Proc:0x007fa75e6830f0@(irb):36>
# You can also register items under namespaces using the #namespace method
container.namespace('repositories') do
namespace('checkout') do
register('orders') { Concurrent::Array.new }
end
end
container.resolve('repositories.checkout.orders')
# => []
# Or import a namespace
ns = Dry::Core::Container::Namespace.new('repositories') do
namespace('authentication') do
register('users') { Concurrent::Array.new }
end
end
container.import(ns)
container.resolve('repositories.authentication.users')
# => []
# Also, you can import namespaces in container class
= Dry::Core::Container::Namespace.new('repositories') do
namespace('authentication') do
register('users') { Concurrent::Array.new }
end
end
# => []
Mixin
You can also get container behaviour at both the class and instance level via the mixin:
extend Dry::Core::Container::Mixin
end
Container.register(:item, :my_item)
Container.resolve(:item)
# => :my_item
include Dry::Core::Container::Mixin
end
container = ContainerObject.new
container.register(:item, :my_item)
container.resolve(:item)
# => :my_item
Registry & Resolver
Register options
call
This boolean option determines whether or not the registered item should be invoked when resolved, i.e.
container = Dry::Core::Container.new
container.register(:key_1, call: false) { "Integer: " }
container.register(:key_2, call: true) { "Integer: " }
container.resolve(:key_1) # => <Proc:0x007f98c90454c0@dry_c.rb:23>
container.resolve(:key_1) # => <Proc:0x007f98c90454c0@dry_c.rb:23>
container.resolve(:key_2) # => "Integer: 157"
container.resolve(:key_2) # => "Integer: 713"
memoize
This boolean option determines whether or not the registered item should be memoized on the first invocation, i.e.
container = Dry::Core::Container.new
container.register(:key_1, memoize: true) { "Integer: " }
container.register(:key_2, memoize: false) { "Integer: " }
container.resolve(:key_1) # => "Integer: 734"
container.resolve(:key_1) # => "Integer: 734"
container.resolve(:key_2) # => "Integer: 855"
container.resolve(:key_2) # => "Integer: 282"
Customization
You can configure how items are registered and resolved from the container. Currently, registry can be as simple as a proc
but custom resolver should subclass the default one or have the same public interface.
= { 'old' => 'new' }
container.fetch(key.to_s) {
fallback_key = RENAMED_KEYS.fetch(key.to_s) {
raise Error, "Missing "
}
container.fetch(fallback_key) {
raise Error, "Missing and "
}
}.call
end
end
extend Dry::Core::Container::Mixin
config.registry = ->(container, key, item, options) { container[key] = item }
config.resolver = CustomResolver
end
include Dry::Core::Container::Mixin
config.registry = ->(container, key, item, options) { container[key] = item }
config.resolver = CustomResolver
end
This allows you to customise the behaviour of Dry::Core::Container. For example, the default registry (Dry::Core::Container::Registry) will raise a Dry::Core::Container::Error exception if you try to register under a key that is already used. Should you want to just overwrite the existing value in that scenario, configuration allows you to do so.
Stubbing in tests
To stub your containers call #stub method:
container = Dry::Core::Container.new
container.register(:redis) { "Redis instance" }
container[:redis] # => "Redis instance"
# before stub you need to enable stubs for specific container
container.enable_stubs!
container.stub(:redis, "Stubbed redis instance")
container[:redis] # => "Stubbed redis instance"
Also, you can unstub container:
container = Dry::Core::Container.new
container.register(:redis) { "Redis instance" }
container[:redis] # => "Redis instance"
container.enable_stubs!
container.stub(:redis, "Stubbed redis instance")
container[:redis] # => "Stubbed redis instance"
container.unstub(:redis) # => "Redis instance"
To clear all stubs at once, call #unstub without any arguments:
container = Dry::Core::Container.new
container.register(:redis) { "Redis instance" }
container.register(:db) { "DB instance" }
container.enable_stubs!
container.stub(:redis, "Stubbed redis instance")
container.stub(:db, "Stubbed DB instance")
container.unstub # This will unstub all previously stubbed keys
container[:redis] # => "Redis instance"
container[:db] # => "Redis instance"