Super lightweight dependency resolution, autowiring and lifecycle management.
Or, yet another dependency injection framework.
Using dependency injection decouples your code and makes it more maintainable, readable and testable.
Imagine I have some code like this:
var MadeUpDb = require('madeupdb')
var MyClass = function() {
this._db = new Madeupdb('localhost', 'database_name', 'username', 'password')
}
MyClass.prototype.getTheThings() {
return this._db.query('SELECT foo FROM bar')
}
MyClass
is tightly coupled to MadeUpDb
, which is to say I can't use this class now without having MadeUpDb
available on localhost
and database_name
configured for username:password@localhost
, nor can I use a different implementation (a mock object, in memory db, etc - perhaps for testing purposes).
If instead I did this:
var Autowire = require('wantsit').Autowire
var MyClass = function() {
this._db = Autowire
};
...
Not only is there less boilerplate, MyClass
has been freed from the responsibility of configuring and acquiring a data source (a beard might call this Inversion of Control) which lets me:
- Concentrate on the interesting bits of
MyClass
(e.g.getTheThings()
) - Easily mock behaviour in tests by setting
_db
- Control resources centrally (were I to have two instances of
MyClass
, the can now share a db connection) - Introduce new functionality without changing
MyClass
. Want a connection pool? No problem, want to wrapMadeUpDb
, AOP style? Done. SwapMadeUpDb
forNewHotDB
? Easy.
Amazing, right?
var Autowire = require('wantsit').Autowire,
Container = require('wantsit').Container
var Foo = function() {
// works with this._bar or this.bar
this._bar = Autowire
};
Foo.prototype.doSomething() {
this._bar.sayHello()
}
...
var Bar = function() {
}
Bar.prototype.sayHello() {
console.log('hello!')
}
...
var container = new Container()
container.register('bar', new Bar())
var foo = new Foo()
container.autowire(foo)
container.once('ready', function() {
foo.doSomething() // prints 'hello!'
})
container.create
will instantiate your object and autowire it:
container.create(Foo)
...
Pass a callback function if you require access to your object after creation:
container.create(Foo, function(error, foo) {
...
})
Constructor arguments are also supported by passing an array of arguments:
var Foo = function(message) {
console.log(message)
}
var foo = container.create(Foo, ['Hello world!'])
...
There are optional methods you can implement to be told when things happen.
// called after autowiring and before afterPropertiesSet
Foo.prototype.containerAware = function(container) {
}
// called after autowiring and after containerAware
Foo.prototype.afterPropertiesSet = function([done]) {
}
If you specify an argument to afterPropertiesSet
, you can defer execution of the callback:
// called after autowiring and after containerAware
Foo.prototype.afterPropertiesSet = function(done) {
this._dep(function(error) {
done(error)
})
}
...
container.create(Foo, function(error, foo) {
// will not be invoked until foo._dep (above) has returned
})
afterPropertiesSet
will be invoked on your class after is has been invoked on all dependencies that also declare it:
Foo = function() {}
Foo.prototype.afterPropertiesSet = function(done) {
// do something that takes a while
setTimeout(done, 1000)
}
Bar = function() {
this._foo = Autowire
}
Bar.prototype.afterPropertiesSet = function() {
// will not be invoked until the callback passed to
// foo.afterPropertiesSet has been called
}
...
container.createAndRegister('foo', Foo)
container.createAndRegister('bar', Bar)
N.b. this means that circular dependencies are not allowed as they will never initialise!
By default we wait up to 5000ms for a deferring afterPropertiesSet
to invoke the passed callback. To override this timeout, pass an option to the container constructor:
var container = new Container({
timeout: 10000 // or 0 to disable the timeout
})
Look-ups occur at runtime, so you can switch out application behaviour without a restart:
// create a bar
container.register('bar', function() {
console.log('hello')
})
// this is our autowired component
var Foo = function() {
this._bar = Autowire
}
Foo.prototype.doSomething = function() {
this._bar();
}
// create and autowire it
var foo = container.create(Foo)
container.once('ready', function() {
foo.doSomething() // prints 'hello!'
// overwrite bar
container.register('bar', function() {
console.log('world')
})
container.once('ready', function() {
foo.doSomething() // prints 'world!'
})
})
All autowired properties are converted to non-enumerable fields so we can JSON.stringify
without serialising the entire object graph.
container.createAndRegisterAll(__dirname + '/lib')
To use this, all your components must be in or under the lib directory. Anything that ends in .js
will be newed up and autowired.
No constructor arguments are supported, it's Autowire
all the way down.
Ok, specify a regex as the second argument - anything that matches it will be excluded
container.createAndRegisterAll(__dirname + '/lib', /excludeme\.js/)
Regex? Great, now I've got two problems. Why stop there? Pass in an array of regexes:
container.createAndRegisterAll(__dirname + '/lib', [/pattern1/, /pattern2/])
Declare them in a file with a lowercase letter. Eg:
// myFunc.js
module.exports = function() {
return true
}
..as opposed to a class which should be in a file that starts with a capital letter:
// MyClass.js
var MyClass = function() {}
MyClass.prototype.foo = function() {
return true
}
module.exports = MyClass
container.createAndRegisterAll(__dirname + '/lib')
// find and invoke our function
var foo = container.find('myFunc')
foo()
// find and invoke a method on our singleton
var myClass = container.find('myClass')
myClass.foo()
No problem, just use the createAndRegisterFunction
method. The first argument is the name to register the
function under, the second is the name of the method on the class, the third is the class constructor. Any
subsequent arguments will be passed to the constructor.
// MyClass.js
var MyClass = function() {}
MyClass.prototype.foo = function() {
return true
}
module.exports = MyClass
container.createAndRegisterFunction('fooFunc', 'foo', MyClass)
// find and invoke our function
var foo = container.find('fooFunc')
foo()
Pass the optional
option to Autowired:
// MyClass.js
var MyClass = function() {
this._foo = Autowired({optional: true})
};
MyClass.prototype.doSomething = function() {
// we are now responsible for making sure this._foo is not null before using it
if(this._foo) {
// ...
}
}
Pass the name
option to Autowired:
// index.js
container.register('bar', {
baz: function() {
return 'hello!'
}
})
// MyClass.js
var MyClass = function() {
this._foo = Autowired({name: 'bar'})
};
MyClass.prototype.doSomething = function() {
// this._foo is 'bar' from the container
this._foo.baz()
}
You can also automate the creation and autowiring of classes
var ObjectFactory = require('wantsit').ObjectFactory
var Foo = function(arg1, arg2) {
this._arg1 = arg1
this._arg2 = arg2
// Autowired fields will be populated automatically
this._dep = Autowired
}
// place an instance of the factory in the container
container.createAndRegister('fooFactory', ObjectFactory, [Foo]);
// MyClass.js
var MyClass = function() {
this._fooFactory = Autowired
}
MyClass.prototype.doSomething = function() {
this._fooFactory.create(['one', 'two'], function(error, foo) {
foo._arg1 // 'one'
foo._arg2 // 'two'
})
}
## Events
The error
event will be emitted if a component throws an error event during invocation of it's constructor, or afterPropertiesSet
/containerAware
methods, or if it passes an error object to the the afterPropertiesSet
callback. If a callback was passed to the create
method, it will receive the error instead:
// class that throws an error during construction
var Foo = function() {
throw new Error('Panic!')
}
...
// no callback passed to `createAndRegister` so container emits error event
var container = new Container()
container.createAndRegister('foo', Foo)
container.on('error', function(error) {
// receives error event
})
...
// here we pass a callback which will receive the error instead
var container = new Container()
container.createAndRegister('foo', Foo, function(error) {
// receives error event
})
The ready
event will be emitted once all currently registered components have initialised. If you subsequently register new components, this event will fire again.
The container will be passed as the first argument to listeners.
var container = new Container({
// an optional logger (e.g. Winston). Defaults to the console.
logger: {
info: function(message) {},
warn: function(message) {},
error: function(message) {},
debug: function(message) {}
},
// how long to wait for deferred `afterPropertiesSet` methods to invoke the callback
timeout: 5000
})
container.register(name, component)
Store a thing
container.find(name)
Retrieve a thing - can by by name (e.g. 'foo'
) or by type (e.g. Foo
)
container.autowire(component)
Autowire a thing
container.create(constructor, [arg1, arg2...], callback)
Create and autowire a thing
container.createAndRegister(name, constructor, [arg1, arg2...], callback)
Create, autowire and register a thing
container.createAndRegisterAll(path, excludes)
Create, autowire and register anything under path
that doesn't match excludes
In create
and createAndRegister
above, arg1, arg2...
are passed to constructor
container.setLogger(logger)
Override the default logging implementation - the passed object must have info
, warn
, error
and debug
methods.