Dependency Injection in NodeJS with Forge and Smithy
Who needs dependency injection in JavaScript?
Coming from the .NET community, I am well versed in the benefits of using an Inversion of Control container. This really allows you to focus on the single responsibility principle and makes unit testing easier. For a while now I have been working with NodeJS and learning its patterns and practices. Many argue the use of an IoC in a dynamic language like JavaScript is unnecessary since you can take advantage of duck typing.
Existing frameworks
While I haven't been writing NodeJS applications for long, I already miss the presence of an IoC. I started looking into this even more after using AngularJS. AngularJS is a great client side framework and is built around registering components within modules and handles resolving of dependencies automatically based on function parameter names or named dependency hints in a array. After looking further I found that Vojta Jina has created a NodeJS library for this called node-di. This is very similar to how angular works, as it should be, since Vojta is also one of the main engineers behind angular.
It works relatively well but didn't feel as fluid or as familiar to what I am used to using. Then, I notice Nate Kohari has also recently started a new NodeJS project called forge. If you aren't familiar with Nate already, he was the original creator of ninject, an IoC container for .NET. I was immediately excited about this when I saw it as I have used ninject on projects before and knew it would feel more familiar and fluid to me coming from a .NET background.
Forge is pretty young at this point. Nate is open about the fact that it is early in its development and probably not production-ready. It is still something you can pull down and play with and Nate is contributing to it frequently.
So what is Smithy?
As I said, I liked what I saw with forge right away. As Nate said, it is young and evolving. I am heavily using TypeScript and so I initially started writing TypeScript declarations for Forge. I realized it doesn't quite have the extensibility of registering separate modules for bootstrapping that I am used to. I like to keep module registration separated into logical groupings and closer to their types rather than in a central location. That is the goal of Smithy.
How do I get it?
npm install forge-di-smithy
-
Equipment
An array of tools that can be ready by the Blacksmith.blacksmith.registerEquipment(equipment);
-
Blacksmith
The main container that will wrap a forge instance and register equipment to bindings on the forge instance.new Smithy.Blacksmith(forge);
-
Tools
A tool is a mapping for a binding. There are 3 different types of tools. Each tool requires a name and a target to be passed as the first 2 arguments respectively. After that, you can pass in a lifecycle, hint, binding arguments, and a when predicate. These are currently configured in 9 different overloads. Will refactor these out into an options object soon.
UPDATE! [2014.04.20]
I have refactored out the overloads into a options object. This options object follows the following contract.{ name: string; target: T; lifecycle?: Lifecycle; when?: Forge.IPredicate; hint?: string; bindingArguments?: Forge.IBindingArguments; }
-
Type Tool
The type tool is a mapping to theforge.bind(...).to.type(...);
registration. -
Function Tool
The function tool is a mapping to theforge.bind(...).to.function(...);
registration. -
Instance Tool
The instance tool is a mapping to theforge.bind(...).to.instance(...);
registration.
-
Example
Here is a brief example. Best example can be found by going to specs/lib/
TypeScript
import Forge = require('forge-di');
import Smithy = require('forge-di-smithy');
class Foo { }
class Foo2 extends Foo { }
class Bar {
constructor(foo: Foo) {...}
}
class Blah {
constructor(public dependency: Foo) {
"dependency->foo2";
}
}
var equipment: Smithy.IEquipment = [
new Smithy.Tools.Type({name: 'foo', target: Foo}),
new Smithy.Tools.Type({name:'foo2', target: Foo2}),
new Smithy.Tools.Type({name: 'bar', target: Bar}),
new Smithy.Tools.Type({name: 'blah', target: Blah})
];
...
var forge = new Forge();
var blacksmith = new Blacksmith(forge);
blacksmith.registerEquipment(equipment);
...
var boo = forge.get('foo');
var blah = forge.get('blah');
expect(boo).to.be.an.instanceOf(Foo);
expect(blah).to.be.an.instanceOf(Blah);
expect(blah.dependency).to.be.an.instanceOf(Foo2);
JavaScript
var Forge = require('forge-di');
var Smithy = require('forge-di-smithy');
function Foo () { }
function Foo2 () { }
function Bar (foo) {
this.foo = foo;
}
function Blah (dependency) {
"dependency->foo2";
this.dependency = dependency;
}
var equipment = [
new Smithy.Tools.Type({name: 'foo', target: Foo}),
new Smithy.Tools.Type({name:'foo2', target: Foo2}),
new Smithy.Tools.Type({name: 'bar', target: Bar}),
new Smithy.Tools.Type({name: 'blah', target: Blah})
];
...
var forge = new Forge();
var blacksmith = new Blacksmith(forge);
blacksmith.registerEquipment(equipment);
...
var boo = forge.get('foo');
var blah = forge.get('blah');
expect(boo).to.be.an.instanceOf(Foo);
expect(blah).to.be.an.instanceOf(Blah);
expect(blah.dependency).to.be.an.instanceOf(Foo2);