wantsit
Super lightweight dependency resolution, autowiring and lifecycle management.
Or, yet another dependency injection framework.
Wat?
Using dependency injection decouples your code and makes it more maintainable, readable and testable.
Imagine I have some code like this:
var MadeUpDb = var { this_db = 'localhost' 'database_name' 'username' 'password'} MyClassprototype return this_db
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 = Autowire var { 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?
I'm sold, show me an example
var Autowire = Autowire Container = Container var { // works with this._bar or this.bar this_bar = Autowire}; Fooprototype this_bar ... var { } Barprototype console ... var container = container var foo = container foo // prints 'hello!'
Lifecycle management
container.create
will instantiate your object and autowire it:
container...
Pass a callback function if you require access to your object after creation:
container
Constructor arguments are also supported by passing an array of arguments:
var { console} var foo = container...
Magic methods
There are optional methods you can implement to be told when things happen.
// called after autowiring and before afterPropertiesSetFooprototype { } // called after autowiring and after containerAwareFooprototype { }
If you specify an argument to afterPropertiesSet
, you can defer execution of the callback:
// called after autowiring and after containerAwareFooprototype { this}... container
afterPropertiesSet
will be invoked on your class after is has been invoked on all dependencies that also declare it:
{}Fooprototype { // do something that takes a while } { this_foo = Autowire}Barprototype { // will not be invoked until the callback passed to // foo.afterPropertiesSet has been called} ... containercontainer
N.b. this means that circular dependencies are not allowed as they will never initialise!
Initialisation timeouts
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 = timeout: 10000 // or 0 to disable the timeout
Dynamic getters
Look-ups occur at runtime, so you can switch out application behaviour without a restart:
// create a barcontainer // this is our autowired componentvar { this_bar = Autowire} Fooprototype { this;} // create and autowire itvar foo = container foo // prints 'hello!' // overwrite barcontainer foo // prints 'world!'
All autowired properties are converted to non-enumerable fields so we can JSON.stringify
without serialising the entire object graph.
I want to register all of the things!
container
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.
Woah, not literally all of the things
Ok, specify a regex as the second argument - anything that matches it will be excluded
container
Regex? Great, now I've got two problems. Why stop there? Pass in an array of regexes:
container
I want to have functions automatically registered
Declare them in a file with a lowercase letter. Eg:
// myFunc.jsmodule { return true}
..as opposed to a class which should be in a file that starts with a capital letter:
// MyClass.jsvar {} MyClassprototype { return true} moduleexports = MyClass
container // find and invoke our functionvar foo = container // find and invoke a method on our singletonvar myClass = containermyClass
I want to register a function on a class
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.jsvar {} MyClassprototype { return true} moduleexports = MyClass
container // find and invoke our functionvar foo = container
My dependency is optional
Pass the optional
option to Autowired:
// MyClass.jsvar { this_foo = }; MyClassprototype { // we are now responsible for making sure this._foo is not null before using it ifthis_foo // ... }
I don't want to use the property name to resolve my dependency
Pass the name
option to Autowired:
// index.jscontainer // MyClass.jsvar { this_foo = }; MyClassprototype { // this._foo is 'bar' from the container this_foo}
Object Factories
You can also automate the creation and autowiring of classes
var ObjectFactory = ObjectFactory var { this_arg1 = arg1 this_arg2 = arg2 // Autowired fields will be populated automatically this_dep = Autowired} // place an instance of the factory in the containercontainer; // MyClass.jsvar { this_fooFactory = Autowired} MyClassprototype { this_fooFactory}
Events
error
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 constructionvar { throw 'Panic!'} ... // no callback passed to `createAndRegister` so container emits error eventvar container = containercontainer ... // here we pass a callback which will receive the error insteadvar container = container
ready
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.
Full API
Constructor
var container = // an optional logger (e.g. Winston). Defaults to the console. logger: {} {} {} {} // how long to wait for deferred `afterPropertiesSet` methods to invoke the callback timeout: 5000
Methods
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.