habitjs v0.5.2
#Habit. Javascript Dependency Injection for the Browser and Node.
##Introduction
Habit is a dependency injection framework for the browser and node,
and a bit of a work in progress. In its present form it is
intended to be used in conjunction with uglify or similar to
allow development across multiple source files and to arrange for and the inclusion
of source maps. (Not required for nodejs) In the future I would like to roll those features
into habit itself along with making it compatible with Node.js (done!).
Habit is heavily inspired by Dependable
but I decided to write my own version because I wanted it to work in a
browser.
##Why? Dependency Injection and Mocking. Modules are a side effect.
##Usage
Defining Modules
A module definition has the following pattern:
supply(
'moduleName',
['Array', 'of', 'dependency', 'names'],
<module value>);
Where module value is either a value or a factory function.
For example...
supply('word', [], 'bird');
... defines a module named word with the String
value 'bird'.
supply(
'message',
['word'],
function (word) {
return word + ' is the word.';
});
... defines a second module named 'message' which uses a factory function to return the string 'bird is the word.' The factory function will only be called once (unless its value is mocked/unmocked or it is used inside a dependency injected closure. See below)
Finally a third module can be defined...
supply(
'speak',
['message'],
function (message) {
return function () {
console.log(message);
};
});
... which returns a function which, when called, logs the message 'bird is the word' to the console.
Out of order definitions.
As modules in habit are resolved lazily they can be defined out of order so...
supply(
'sayHello',
['name'],
function (name) {
return function () {
alert('Hello, ' + name + '.');
};
});
... followed by...
supply(
'name',
[],
'World'
);
... would work perfectly well because the dependencies of the 'sayHello' module are not resolved until it itself is required somewhere else.
Using Modules
Modules can be resolved two ways. If you want to resolve several modules at once you can use a callback.
need(['speak', 'sayHello'], function (speak, sayHello) {
speak();
sayHello();
});
Alternatively, as habit is synchronous, modules can be resolved and returned by the need
function
need('speak')();
Module names and relative paths.
Module names can be any string you like, but to aid in namespacing and grouping related modules, when path-like modules
names such as path/to/my/awesome/module
are used, the module can declare its dependency using relative path names
so...
supply(
'my/awesome/module',
[
'../really/special/function',
'./string'
],
function (fun, str) {
//Some specially awesome code here...
});
... would define a module with dependencies on the modules my/really/special/function
and
my/awesome/string
.
Mocking and Unmocking
To aid in testing, modules can be temporarily mocked and unmocked using Habit.mock/Habit.unmock
Given a module:
supply(
'name',
[],
'Dave'
);
And a second module:
supply(
'married',
['name'],
function (name) {
return 'You\'re my wife now, ' + name;
});
I can mock the value of the name module with
Habit.mock('name', 'Brian');
Mocking a module causes all modules which rely on it (directly or indirectly) to be re-resolved next time they are required so
console.log(need('married'));
Would output "You're my wife now, Brian", to the console.
The 'name' module can then be unmocked using:
Habit.unmock('name');
Which again causes all the name modules dependences to be re-resolved so...
console.log(need('married'));
... would once again log "You're my wife now, Dave" to the console
An important point to note is that mocked values are used as is. Functions supplied as mocks will be returned as functions, not the results of said functions.
If you have a lot of modules mocked they can all be unmocked at the same time using Habit.unmockAll()
Dependency Injection Closures
While habit by default has a single context, separate contexts can by created by passing a third parameter to the need function.
Given modules:
supply(
'name',
[]
'World',
);
supply(
'message',
['name'],
function (name) {
return 'Hello, ' + name + '!';
});
supply(
'tell',
['message'],
function (message) {
return function () {
console.log(message);
};
});
Then the following code...
var inject = {
'name': 'Mum'
};
function callback (tell) {
tell();
}
need(['tell'], callback, inject);
... would write 'Hello, Mum!' to the console, whereas the code...
var inject = {
'message': function (name) {
return 'Goodbye, cruel ' + name + '!';
}
};
function callback (tell) {
tell();
}
need(['tell'], callback, inject);
... would write 'Goodbye, cruel World!' to the console and the code...
var inject = {
'tell': function (message) {
alert(message);
}
};
function callback (tell) {
tell();
}
need(['tell'], callback, inject);
... would display an alert with the message 'Hello, World!'
####How do Dependency Injection closures work?
By default Habit has a single context. All modules are defined within the same space, but when you create a dependency injection closure, the context is cloned. Within the closure all the modules are reset so factory functions will be re-run when the modules are resolved. Any value passed in the inject object will replace the value of the original module. In fact, since requirements are lazily resolved, module values can be provided in the inject object which have never been defined in the main context.
####Creating, resolving, mocking and unmocking modules inside dependency injection closures.
The supply
and need
global functions and the Habit.mock
and Habit.unmock
functions always refer to the main
context. To enable the use of these functions inside a dependency injection closure a special module 'local' can
be required.
var context = {
}
var callback = function (local) {
local.mock('name', 'fred');
console.log(need('name'));
local.unmock('name');
console.log(need('name'));
}
need([], callback, context);
The 'local' module has mock, unmock, supply, need, unmockAll, disolve and disolveAll functions.
Circular Dependencies
Being a synchronous framework, Habit does not have any good way to deal with circular dependencies at resolve time so code such as...
supply(
'parent',
['child'],
function (child) {
var name = 'Parent'
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + child.name;
};
}
});
supply(
'child',
['parent'],
function (parent) {
var name = 'Child';
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + parent.name;
}
};
});
need([parent], function (parent) {
//code in here will never run
});
... would throw an error when Habit tried to resolve the parent module. These sorts of dependencies are only an issue at resolve time though. Refactoring the code as follows would solve the issue.
supply(
'parent',
[],
function () {
var name = 'Parent'
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + need('child').name;
};
}
});
supply(
'child',
[],
function () {
var name = 'Child';
return {
name: name,
sayHello: function () {
return 'Hello, my name is ' + name + ' and I depend on ' + need('parent').name;
}
};
});
need(['parent', 'child'], function (parent) {
console.log(parent.sayHello(), child.sayHello());
});
Usage with node.js
The NPM Module for habit is called habitjs. Install it with
npm install habitjs
To save you from peppering your code with calls to require Habit will automatically attempt to load locally defined modules with the following caveats.
- You must use path style module names.
- The path style module names must reflect the paths in your project.
- It will only load modules defined in your project and not modules loaded via npm.
The modules will be loaded relative to the path of your main module. If you want to change this then call Habit.setRequireRoot with the new location.
If you want to use habit to load external modules then simply wrap your externals in a Habit module. eg.
//File: ext/fs.js
require('habitjs').supply('ext/fs', [], function () {
return require('fs');
});
This then allows you to mock out modules supplied by npm.
Node API
To prevent you having to type code like:
habit = require('habitjs');
habit.Habit.mock('someModule', 'Hello');
the object returned from require('habitjs') has the following signature.
- supply: ...
- need: ...
- mock: bound to Habit.mock
- unmock: bound to Habit.unmock
- unmockAll: bound to Habit.unmockAll
- disolve: bound to Habit.disolve
- disolveAll: bound to Habit.disolveAll
Habit: ...
Utility Functions
To force a factory function to re-resolve you can use Habit.disolve('moduleName')
You can also force the re-resolution of all modules with Habit.disolveAll()
Change Log
Only started at 0.5.0 (oops!)
v0.5.0 Added automatic requiring under node for mocked modules. Improved syntax for node. v0.5.1 Fixed bug with DI closures under node. v0.5.2 Actually fixed bug with DI closures under node.