Repository is archived
No commit activity in last 3 years
No release in over 3 years
There's a lot of open issues
A fully customizable Dependency injection system for Ruby
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

Dependency Injection for Ruby

Build Status Coverage Status Code Climate

Foreword

This gem is heavily inspired from The Symfony Framework Container Service. Here's the description they give to explain the concept:

It helps you instantiate, organize and retrieve the many objects of your application. This object, called a service container, will allow you to standardize and centralize the way objects are constructed in your application. The container makes your life easier, is super fast, and emphasizes an architecture that promotes reusable and decoupled code.

You can learn more about everything this gem does by looking at the examples directory. See Usage for a detailed explanation.

Description

Installation

Just add the gem to your Gemfile:

gem 'dependency_injection'

Or simply install it using rubygems:

gem install dependency_injection

Example

Without using Dependency Injection

In this example, we'll consider a simple application that needs to send emails for a newsletter.

We have the two following classes:

# mailer.rb
class Mailer
  attr_accessor :transporter

  def initialize
    puts 'mailer initialized'
  end

  def send_mail(message, recipient)
    puts "mail sent via #{self.transporter}: #{message}"
  end
end
# newsletter_manager.rb
class NewsletterManager
  def initialize(mailer)
    @mailer = mailer
  end

  def send_newsletter(message, recipients)
    puts 'newsletter #{message} send to #{recipients}'
    recipients.each { |recipient| @mailer.send_mail(message, recipient) }
  end
end

A Mailer class that handles email sending, through a given transporter, for a recipient.

A NewsletterManager class that sends a newsletter (message) to a list of recipients.

Without DependencyInjection, we would need to do the following to achieve our goal:

# send_newsletter.rb
mailer = Mailer.new
mailer.transporter = :smtp

recipients = %w(john@doe.com david@heinemeier-hansson.com)
message    = 'I love dependency injection and think this is the future!'

newsletter_manager = NewsletterManager.new(mailer)
newsletter_manager.send_newsletter(message, recipients)

You have a working application but this code is thightly coupled and might be, in a real life, hard to refactor.

Another big drawback is that you have to instantiate as many objects as you have emails and newsletters to send.

Now with Dependency Injection

Our two classes stay untouched, the only thing you have to do is to add a configuration file.

# services.yml
parameters:
  mailer.transporter: 'smtp'
services:
  mailer:
    class: 'Mailer'
    calls:
      - ['transporter=', '%mailer.transporter%']
  newsletter_manager:
    class: 'NewsletterManager'
    arguments:
      - '@mailer'

We now need to require DependencyInjection and declare our Container.

Please note that the following code only needs to be declared once as long as the container value is accessible throughout your whole application.

# initialize.rb
require 'dependency_injection/container'
require 'dependency_injection/loaders/yaml'

container = DependencyInjection::Container.new
loader    = DependencyInjection::Loaders::Yaml.new(container)
loader.load(File.join(File.dirname(File.expand_path(__FILE__)), 'services.yml'))

We can now do the same as the previous example with the following.

# send_newsletter.rb
recipients = %w(john@doe.com david@heinemeier-hansson.com)
message    = 'I love dependency injection and think this is the future!'

container.get('newsletter_manager').send_newsletter(message, recipients)

Now your code is no longer tightly coupled and can be a lot more easily refactored. Moreover, the Mailer and NewsletterManager classes are only instantiated once during your application's lifecycle.

Usage

Before diving into the details of DependencyInjection, here are some keywords that you need to be acquainted with:

  • Container object must be declared to be used by your application during it's whole lifecycle. The Container job is to register and retrieve Services.

  • Service is a Plain Old Ruby Object (Poro o/) that contains your own logic. The DependencyInjection gem doesn't need to know anything about it and won't force your to add/inherit any specific method.

  • Configurator is a standard Ruby Class that shares a callable to be used by different objects (like Services) to configure them after their instantiation.

Configuration

DependencyInjection needs to be configured, using a yaml file, in order to map your services with your existing classes and their dependencies. There's also some other options that we'll list below.

Here's a configuration file example using almost everything DependencyInjection has to offer:

parameters:
  mailer.transport: smtp
services:
  mailer:
    class: Mailer
    calls:
      - ['transport=', '%mailer.transport%']
    lazy: true
  newsletter:
    class: NewsletterManager
    arguments:
      - '@mailer'

And here's some more details about each keyword:

  • parameters: Based on a basic key/value scheme. This can later be used throughout your services by calling %parameter_name%.

  • services: The services name must be used as the first indentation tier.

  • class: A string containing the class name of your service.

  • arguments: An array containing the parameters used by your class intialize method.

  • calls: An array containing an array of each instance method and its parameters. Note that you only need to define the methods during your class instantiation.

  • file_path: A string containing the file path to require to load the class.

  • lazy: Returns a Proxy Object if true. The real object will only be instantiated at the first method call.

  • alias: A string containing the target service name.

  • scope: A string containing one of two possibles values to handle the service initialization scope:

    • container: a service is initialized only once throughout the container life (default)
    • prototype: a new service is initialized each time you call the container

    Note that the usage of a prototype service inside a container service raises a ScopeWideningInjectionError

Please note:

  • You can reference a variable in the configuration with the following syntax: %variable%.
  • You can reference declared services by prefixing it with an @ sign.
  • If you declare a service as an alias, the target service configuration will be used. Your own service configuration will be ignored.

Tests

DependencyInjection is covered by tests at 100%, see coveralls.io service.

If you want to launch the tests by yourself:

  • Clone this repository by running git clone git@github.com:kdisneur/dependency_injection-ruby
  • Run bundle install
  • Run rake test

Contribute

This is Github folks!

If you find a bug, open an Issue.

It's OK to open an issue to ask us what we think about a change you'd like to make, so you don't work for nothing :)

If you want to add/change/hack/fix/improve/whatever something, make a Pull Request:

  • Fork this repository
  • Create a feature branch on your fork, we just love git-flow
  • Do your stuff and pay attention to the following:
  • Your code should be documented using Tomdoc
  • You should follow Github's Ruby Styleguide
  • If needed, squash your commits to group them logically
  • Update the CHANGELOG accordingly
  • Make a Pull Request, we will always respond, and try to do it fast.