Gem Version Build Status Dependency Status

Fabrique

Configuration-based factory for dependency injection. Inspired by Java spring beans.

Installation

Add this line to your application's Gemfile:

gem 'fabrique'

And then execute:

$ bundle

Or install it yourself as:

$ gem install fabrique

Testing

git clone [email protected]/starjuice/fabrique.git
cd fabrique
bundle
bundle exec rake

The test coverage is superficial at the moment. When working with features/bean_factory.feature, keep in mind that some exceptions are caught and stored as instance variables to be evlauated by subsequent steps. This avoids some English contortions, but the down side is that if you introduce a regression that raises an exception in a scenario that doesn't expect one, you won't see the exception. To log exceptions, even when they are expected:

DEBUG=1 bundle exec rake

Usage

Under construction; hard hat required!

The best source of documentation of what's possible in the configuration of Fabrique::BeanFactory is currently features/bean_factory.feature. But reading the step definitions for an example of usage would be awkward, so here is a code example.

Given this example YAML application context definition, in a file called application_context.yml:

---
beans:
- id: customer_repository
  class: Acme::Repository::CustomerRepository
  constructor_args:
    - !bean/ref store
    - !bean/ref customer_data_mapper
- id: product_repository
  class: Acme::Repository::ProductRepository
  constructor_args:
    store: !bean/ref store
    data_mapper: !bean/ref product_data_mapper
- id: store
  class: Acme::Repository::MysqlStore
  constructor_args:
    host: localhost
    port: 3306
  gem:
    name: acme-repository-mysql_store
    version: "~> 1.0"
    require: "acme/repository/mysql_store"
- id: customer_data_mapper
  class: Acme::Repository::MysqlDataMapper::Customer
  gem:
    name: acme-repository-mysql_data_mapper
    require: acme/repository/mysql_data_mapper/customer
- id: product_data_mapper
  class: Acme::Repository::MysqlDataMapper::Product
  gem:
    name: acme-repository-mysql_data_mapper
    require: acme/repository/mysql_data_mapper/product

Here is how we could materialize these dependencies:

bean_factory = Fabrique::YamlBeanFactory.new(File.read('application_context.yml'))
bean_factory.load_gem_dependencies

customer_service = CustomerService.new(repository: bean_factory.get_bean('customer_repository'))
product_service = ProductService.new(repository: bean_factory.get_bean('product_repository'))
store_service = StoreService.new(customers: customer_service, products: product_service)
# ...

Of course, the construction of these services could just as well have been handled by the bean factory as well. Where to draw the line is an interesting topic. The example above draws it "at dependencies that could be expected to vary per deployment".

Notice that gems were only specified for the parts of the context that could be expected to vary per deployment; the gem providing Acme::Repository::CustomerRepository and Acme::Repository::ProductRepository would be bundled with the application.

Also notice that the gem loader is activated by an explicit call to load_gem_dependencies. Runtime installation and activation of gems is controversial, and so it must be requested explicitly.

For a data-centric approach to IoC, the entire set of beans can be materialized as a dictionary and made available to consumer to cherry-pick what they need without any awareness of the use of a BeanFactory:

bean_factory = Fabrique::YamlBeanFactory.new('application_context.yml')
bean_factory.load_gem_dependencies

# Symbolize keys for use as keyword arguments:
context = bean_factory.to_h.map { |k, v| [k.intern, v] }.to_h

store_service = StoreService.new(context)