0.5.2 • Published 10 years ago

habitjs v0.5.2

Weekly downloads
1
License
MIT
Repository
github
Last release
10 years ago

#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.

  1. You must use path style module names.
  2. The path style module names must reflect the paths in your project.
  3. 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.

0.5.2

10 years ago

0.5.1

10 years ago

0.5.0

10 years ago

0.4.1

10 years ago

0.4.0

10 years ago

0.3.3

10 years ago

0.3.2

10 years ago

0.3.1

10 years ago

0.3.0

10 years ago