Cation
A fast and customizable Dependency Injection Container for Node.js / io.js
Install / Use
/learn @sergiolepore/CationREADME
Cation
A fast and customizable Dependency Injection Container for Node.js / io.js
[![NPM Version][npm-image]][npm-url] | [![Stack Share][stackshare-image]][stackshare-url] | [![NPM Downloads][downloads-image]][downloads-url] :---: | :---: | :---: [![Build Status][travis-image]][travis-url] | [![Donations][gratipay-image]][gratipay-url] | [![Gitter][gitter-image]][gitter-url]
var Cation = require('cation')
var container = new Cation()
var Atom = function() { /* Atom service! */ }
container.register('Atom', Atom)
container.get('Atom').then(function(atom) {
/* new atom object! */
})
Brief intro
Cation is a powerful Dependency Injection Container (DIC). The first version was released in 2011-2012 as an unstable/experimental library and was inspired by the Symfony 2 container. [It only allowed a JSON schema to register a service (yeah, it was an ugly experiment)][original-cation-url]. There were no Factories, no Decorators. Just "Services".
The version 2 is a MUCH, MUCH BETTER EVOLUTION, heavily inspired by these projects:
- [Symfony DIC][symfony-doc-url] - [API][symfony-api-url]
- [Dependency Injection: the Ember.js way][ember-doc-url] - [API][ember-api-url]
- [Dependency Injection: the Angular.js way][angular-doc-url] - [API][angular-api-url]
Cool things you'll enjoy for sure:
- Organize all your dependencies in a single place. Retrieve them only when you need them.
Service,FactoryandStaticresource providers, right out of the box.Decoratorsupport.- User-defined / Custom resource providers.
- Lazy loaded dependencies (and it's awesome).
- Future-proof JavaScript / ES6 support.
- Beautiful API.
Installation
$ npm install cation
Usage
Services
Simple Services
This is probably one of those things you are doing a lot, creating new objects. I'm going to reuse the Atom example from the top of this file and explain with simple words what happens when you use Cation.
This is the constructor for a service object:
var Atom = function() { /* Atom constructor */ }
And this is how you register the Atom constructor in the container:
var Cation = require('cation')
var container = new Cation()
container.register('Atom', Atom)
Boom. It's done. If you need a new object:
container.get('Atom').then(function(atom) {
// a new Atom instance over here!
})
// or...
var getAtomPromise = container.get('Atom')
getAtomPromise.then(function(atom) {
// a new Atom instance over here!
})
So, what happened here?
- You call
Cation#register. - Cation creates a new
ServiceProviderobject with the arguments you provided. - Cation stores the provider object in an internal repository.
- Done with the register process.
- Now you call
Cation#get. - Cation returns a new
Promiseobject. - You call
Promise#thenand pass a callback function that will be executed when the promise is resolved. - In the meantime, Cation looks in the repository for the desired provider.
- Cation asks the provider to digest the original arguments and to return something.
- The
ServiceProviderinternally creates a new object from the constructor function you provided in the first place and returns it. - Cation resolves the previous promise with the object returned by the provider.
- You have your service object.
Dependency Injection
The main purpose of a Dependency Injection Container. Imagine you need to create a chemical element object, with a few arguments:
var Element = function(name, symbol, atomicNumber, atomicWeight) {
this.name = name
this.symbol = symbol
this.atomicNumber = atomicNumber
this.atomicWeight = atomicWeight
}
container.register('Hydrogen', Element, {
args: ['Hydrogen', 'H', 1, 1.008]
})
container.get('Hydrogen').then(function(element) {
console.log(element.name) // Hydrogen
})
What if one or more arguments are dependencies to other services? Let's take a look at a little more complex example:
// HEADS UP!
//
// For the sake of "simplicity", I'm going to use a few ES6 features in this example.
//
// The third argument here, ...elements, is known as a "rest parameter" and is part of the ES6 specification.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
var Compound = function(name, formula, ...elements) {
this.name = name
this.formula = formula
this.elements = elements
this.getMolarMass = function() {
// HEADS UP!
// I'm using "arrow functions" here. It's also part of ES6.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
return this.elements
.map(element => element.atomicWeight)
.reduce((accumulator, weight) => accumulator + weight)
}
}
container.register('Hydrogen', Element, {
args: ['Hydrogen', 'H', 1, 1.008]
})
container.register('Oxygen', Element, {
args: ['Oxygen', 'O', 8, 15.999]
})
// This. Read it carefully...
container.register('Water', Compound, {
args: ['Water', 'H2O', '@Hydrogen', '@Oxygen', '@Hydrogen'] // notice the "@"?
})
container.get('Water').then(function(molecule) {
console.log(molecule.formula) // H2O
console.log(molecule.getMolarMass()) // 18.015
})
Sweet, isn't it? Just remember: you can use whatever you want as an argument. But if you need an argument that is a registered resource in Cation, you need to reference it by using a string like "@MyRegisteredResource".
Q: What if I need, in fact, an argument that is a string and starts with the @ character but it's not a resource reference?
A: You can escape it with double backslashes.
container.register('Something', Something, {
args: ['@ServiceReference', '\\@NormalStringArgument']
})
By doing this, your service will receive a new ServiceReference object as a first argument and "@NormalStringArgument" as a second argument.
Psst! [Click here][es6example-services-01] to see this example using ES6 syntax.
Singletons
Enabling this pattern is as easy as setting an option named singleton to true.
container.register('SingletonService', Service, {
singleton: true
})
A note about Service register options
As you can see, the register method can take up to three arguments:
- id: resource identifier.
- resource: the resource to be registered (in this case, the service constructor).
- options: an object containing options.
When you are registering a service, the available options are:
type: this option is always provided by default asservice. You can replace it withtype: 'factory'and you'll be registering a factory, instead of a service. You'll learn about this in the next topic.singleton: boolean option andfalseby default. If set to true, Cation will treat the resource as a singleton.args: array option,[]by default. These are the arguments to apply to the service constructor. It only works if thetypeisservice.decorators: array option,[]by default. These are the names of theDecoratorsto be applied to the returned objects. You'll learn about this in theDecoratorstopic.tags: array option,[]by default. You can use this feature to tag your services. You'll learn about this inWorking with tagged resourcessection.
Having known that, these options are the same:
{
type : 'service',
singleton : true,
args : ['a', 2, '@B'],
decorators : ['decorator1', 'decorator2']
}
{
singleton : true,
args : ['a', 2, '@B'],
decorators : ['decorator1', 'decorator2']
}
{
args : []
singleton : true
}
{
singleton: true
}
Factories
If you need a more granular control over your created objects, you can opt for the Factory Pattern. In Cation, a factory must follow these simple rules:
- The factory function receives the container object as an optional argument. Useful only if you need dependencies from it.
- The factory function must return a Promise.
- The
Cation#registermethod must receive anoptionsobject with, at least, a propertytypeequals tofactory.
// if you know how promises work, you'll notice that this code block can be
// simpliflied by just returning the execution of Promise.all().then().
// I'll keep this example to explicitly show the returned promise.
container.register('ServiceC', function(container) {
var factoryPromise = new Promise(function(resolveFactoryPromise, rejectFactoryPromise) {
var depencencyAPromise = container.get('ServiceA')
var dependencyBPromise = container.get('ServiceB')
// main promise will resolve when all dependency promises are resolved
Promise.all([dependencyAPromise, dependencyBPromise]).then(function(services) {
// our resolved services
var dependencyA = service[0]
var dependencyB = service[1]
dependencyA.doSomething()
dependencyB.property = 'Something Else'
resolveFactoryPromise(new ServiceC(dependencyA, dependencyB))
}).catch(function(error) {
rejectFactoryPromise(error)
})
})
return factoryPromise
}, {
type: 'factory'
})
container.get('ServiceC').then(function(serviceObject) {
// do something with serviceObject
})
// and this is the simplified version of the above implementation
container.register('ServiceC', function(container) {
var depencencyAPromise = container.get('ServiceA')
var dependencyBPromise = container.get('ServiceB')
// we return the promise chain.
// remember, if you return something inside a promise, you'll get a promise whose
// resolved value is whatever you returned in the latter.
// `promise chaining` if you want to call it by a name.
return Promise.all([dependencyAPromise, dependencyBPromise]).then(function(services) {
// our resolved services
var dependencyA = services[0]
var depen
