LittleBoxes
Dependency injection library in Ruby.
Intro
LittleBoxes allows you to create a box which is capable of providing already
configured objects that depend among each other.
module MyApp
class MainBox
include LittleBoxes::Box
let(:port) { 80 }
letc(:server) { Server.new }
end
class Server
include LittleBoxes::Configurable
dependency :port
end
end
box = MyApp::MainBox.new
box.server.port
The let
keyword provides lazy evaluated dependencies:
class MainBox
include LittleBoxes::Box
let(:redis) do
require 'redis'
Redis.new
end
end
box = MyApp::MainBox.new
box.redis
Notice that in this situation require 'redis'
will not evaluated until we
call box.redis
. This approach, in opposition to initializers brings two
benefits:
- Libraries are only loaded when needed. Unused libraries are not loaded.
I. e. when running only a subset of the tests.
- There is no need to manually set the order of the initializers, it will be
resolved at run-time.
Dependencies can rely on other dependencies:
class Publisher
attr_accessor :redis
def initialize(redis: redis)
@redis = redis
end
end
class MyApp::MainBox
include LittleBoxes::Box
let(:redis) do
require 'redis'
Redis.new
end
let(:publisher) do |box|
Publisher.new redis: box.redis
end
end
box = MyApp::MainBox.new
box.publisher
However, what kind of dependency injection library would this be if dependencies
wouldn't be automatically resolved. We can use the letc
method for that:
class Publisher
include LittleBoxes::Configurable
dependency :redis
end
class MainBox
letc(:publisher) { Publisher.new }
end
Configurable objects accept default values passed as a lambda, which receives
the box as an argument:
class Server
include LittleBoxes::Configurable
dependency(:port) { 80 }
dependency(:log) { |box| box.logger }
end
If classes instead of instances are your thing, not that I recommend it, the
class_depency
will do:
class UsersApi
include LittleBoxes::Configurable
class_dependency :logger
end
class MainBox
letc(:users_api) { UsersApi }
end
Working with classes you might find the problem that they tend to be accessed
directly through the constant, not injected. This totally skips the lazy
configuration we discussed before.
For those cases we can force the Box to eager-configure such dependency:
class MainBox
eager_letc(:users_api) { UsersApi }
end
Now the class will be configured as soon as you do MainBox.new
.
Sometimes we don't want the box to memoize our dependencies and we want it
to execute the lambda each time. This were get
and getc
have a role:
class MainBox
get(:config) { { port: 80 } }
getc(:new_server) { Server.new }
end
Boxes can be nested, allowing to create a better arranged tree of dependencies.
Very useful as your application grows:
class MainBox
box(:users) do
letc(:users_api) { UsersApi }
let(:logger) { Logger.new('/tmp/users.log') }
end
let(:logger) { Logger.new('/tmp/app.log') }
end
Notice how in this case, any object inside the users
box will log to
users.log
. Dependencies are resolved recursively up to the root box.
Throwing a LittleBoxes::DependencyNotFound
when missing at root level.
To avoid defining a huge single Box file, you can import boxes defined elsewhere:
class UsersBox
letc(:users_api) { UsersApi }
end
class MainBox
import UsersBox
let(:logger) { Logger.new('/tmp/app.log') }
end
Contributing
Do not forget to run the tests with:
rake
And bump the version with any of:
$ gem bump --version 1.1.1
$ gem bump --version major
$ gem bump --version minor
$ gem bump --version patch
License
Released under the MIT License.
See the LICENSE file for further details.