Dependency injection framework in Ruby
Ruby
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
assets
lib
spec
tmp
.gitignore
.travis.yml
Gemfile
LICENSE.txt
README.md
Rakefile
little_boxes.gemspec

README.md

LittleBoxes

Gem Version Build Status

LittleBoxes

LittleBoxes is a light library that helps to do dependency injection in Ruby. It's main features are:

  • Provides a dependency tree that represents your application configuration.
  • Automatically configures your dependencies.
  • Lazy-loads by default.

For an introduction to it you can watch the talk below, given at the London Ruby User Group. Slides are here http://slides.com/manuelmorales-1/little-boxes-1.

Dependency Injection with LittleBoxes at LRUG

Also, there is a Todo-Backend app built with LittleBoxes in https://github.com/manuelmorales/todo-backend-little-boxes.

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
# => #<MyApp::MainBox :port, :server>

box.server.port
# => 80

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
# => #<MyApp::Redis:0x0055acd05b6350>

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
# => #<MyApp::Publisher:0x0055df32201ae0 @redis=#<MyApp::Redis:0x0055df32201b30>>

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
  # ...
  eagerc(: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       # Bump the gem version to the given version number
$ gem bump --version major       # Bump the gem version to the next major level (e.g. 0.0.1 to 1.0.0)
$ gem bump --version minor       # Bump the gem version to the next minor level (e.g. 0.0.1 to 0.1.0)
$ gem bump --version patch       # Bump the gem version to the next patch level (e.g. 0.0.1 to 0.0.2)

License

Released under the MIT License. See the LICENSE file for further details.