uqbar

0.1.2 • Public • Published

uqbar is a simple IoC (Inversion of Control) container for javascript.

This module is inspired a lot in the simplicity of the Funq Container for the .Net Framework from Daniel Cazzulino.

Installation

npm i uqbar

What you need to know

Create an instance of Container as follows:

const Container = require('uqbar').Container;
const container = new Container();

A container has 2 important APIs register and resolve.

Register is the API to wire the services in the container. A simple registration looks like this:

container.register('myservice', (callback) => {
  callback(null, { the_serivce: '123'});
})

It receives a name and a factory function with a callback. register is always async but there are three sync shorthands:

  • registerSync(name, syncfactory, [options])
  • registerCtor(name, ctor, [options])
  • registerInstance(name, instance, [options])

Resolve creates an instance of a service and it is always async:

container.resolve('myservice', (err, myservice) => {
  console.log(myservice);
})

Everything in uqbar builds on top of these two methods.

Singleton vs transient

register assumes by default that the service is singleton:

var i = 0;
container.registerSync('service', () => ++i);
container.resolve('service', (err, service) => {
  assert.equal(service, 1);
  container.resolve('service', (err, service) => {
    assert.equal(service, 1); ///is still 1.. because the factory is called once.
  });
});

If you want to register a "transient" service:

var i = 0;
container.registerSync('service', () => ++i, { singleton: false });
container.resolve('service', (err, service) => {
  assert.equal(service, 1);
  container.resolve('service', (err, service) => {
    assert.equal(service, 2); ///yeah now is 2
  });
});

Brewing machine example

How boring will be a container if it couldn't handle dependencies!

Given a set of classes like this:

function Heater() {}

function Pump() {}

function CoffeeMaker(heater, pump) {
  this.heater = heater;
  this.pump = pump;
}

CoffeeMaker.prototype.brew = function () {
  //this.pump.pump();
  //this.heater.on();
}

In order to understand the primitives of the API this is the most verbose way of registering these services:

container.register('heater', callback => callback(null, new Heater()));
container.register('pump', callback => callback(null, new Pump()));
container.register('coffee_maker', callback => {
  container.resolve(['heater', 'pump'], (err, services) => {
    if (err) { return callback(err); }
    callback(null, new Pump(services[0], services[1]));
  })
});

The container supports auto resolution of dependencies, so the above can be written as follows:

container.registerSync('heater', () => new Heater());
container.registerSync('pump', () => new Pump());
container.registerSync('coffee_maker', (heater, pump) => new CoffeeMaker(heater, pump));

And the less verbose way looks like this:

container.registerCtor('heater', Heater);
container.registerCtor('pump', Pump);
container.registerCtor('coffee_maker', CoffeeMaker);

Database example

Let's say we have a module called database.js like this:

module.exports = function(mysql, settings, callback) {
  var connection = mysql.createConnection({
    host: settings.HOST,
    port: settings.PORT
  });

  connection.connect((err) => callback(err, connection));
}

and a todo.js like this:

function Todos (db) {
  this.db = db;
};

Todos.prototype.find = function () {
  db.query('SELECT * from todos', callback);
};

module.exports = Todos;

We wire the application like this:

//register the "settings" service
container.registerInstance('settings', process.env);

//register all node_modules (including mysql)
fs.readdirSync('./node_modules')
  .filter(d => d !== '.bin')
  .forEach(m => {
    container.registerSync(m, () => require(m));
  })

//register "db"
container.register('db', require('./lib/database'));

//register "todos"
container.registerCtor('todos', require('./lib/todos'));

Interfaces examples

A common pattern is to inject several instances implementing a common interface.

The most common example of this is a logger receiving an array of streams:

function logger (bunyan, loggerStream) {
  return bunyna.create({ streams: loggerStream });
}

Register the streams as follows:

container.registerSync('file_stream',
                        settings => fs.createWriteStream(settings.LOG_FILE),
                        { interfaces: ['loggerStream']});

container.registerSync('stdout_stream',
                       () => process.stdout,
                       { interfaces: ['loggerStream']});

container.registerSync('logger', logger);

Note: Resolving an interface name always returns an Array.

Dependency binding

The register API can figure out the dependencies of a service. Currently it supports two methods.

The default method is to use the names of the parameters of the function. For instance if we register a function like this:

function logger(bunyan, loggerStream) {}

The container assumes that the logger service depends on the bunyan service and the loggerStream service.

Another way is to expose a @require property in the function like this:

function logger(bunyan, streams) {
  //...
}

logger['@require'] = ['bunyan', 'loggerStream'];

In this case logger depends on bunyan and loggerStream regardless that the parameter name of the function is streams.

License

MIT - Copyright (c) 2016 JOSE FERNANDO ROMANIELLO.

See the LICENSE file.

Readme

Keywords

none

Package Sidebar

Install

npm i uqbar

Weekly Downloads

7

Version

0.1.2

License

none

Last publish

Collaborators

  • jfromaniello