… or how to leverage AMD’s require/define semantics for fine-grained dependency injection.
update: code now available on gist.github.com: https://gist.github.com/4089076
update2: better implementation of calling the service constructor in resolveInstance() to follow operator new semantics.
i’ve become a big fan of the asynchronous module definition (AMD) pattern for managing dependencies between libraries and script files in large JavaScript projects. requirejs is my current favourite AMD library. TypeScript also provides excellent support for compiling and consuming modules in AMD-compatible formats.
i’ve also become a big fan of Ninject for its lightweight but very effective dependency injection library for .Net.
when looking for a simple Ninject-like dependency injection container for JavaScript, I attempted to find something that would be as familiar as using the requirejs/AMD approach. I thought about using the requirejs plugin API, but it is too resource/file oriented and would require resource naming contortions to support named identities for containers and object instances.
during my research I came across Coffeescripter’s post regarding dependency injection and IoC in javascript, and I admired the simple and straightforward modeling of the relationship between an array of dependency names and a constructor function. To me, this approach represented a very effective and lightweight way to adapt existing code for use in a dependency injection framework without having to make any modifications to that code. The constructor functions act as shims between the dependency injection framework and existing code, and work much the same way as the requirejs/AMD define() function does with its associated callback.
this inspired me to implement a dependency injection container using define()/require()-like semantics, including support for circular dependency resolution.
the source…
you can download the source files for the ServiceKernel class described in this post, including unit tests, from here: https://gist.github.com/4089076
note: the source code provided has a few dependencies on ECMAScript 5 features, such as Array.isArray, Array.prototype.indexOf, and Function.prototype.bind. You can easily find replacements for these functions should you need to target an older platform.
a short example
to whet your appetite, here is a simple example of defining some services in a container and then retrieving them.
// create the kernel
var kernel = new ServiceKernel();
// define service A, which has no dependencies
kernel.define("A",
function() {
// service A has a function named foo
return {
foo: function () { return "foo"; }
}
});
// define service B, which depends on A
kernel.define("B", ["A"],
function(a) {
// service B has a function named foobar
// which uses the dependency service A passed in
// as parameter a
return {
foobar : function() { return a.foo() + "bar"; }
}
});
// get instance of service B
var b = kernel.require("B");
b.foobar(); // returns "foobar"
// get both A and B services instances,
// which are passed to the callback
// as parameters a and b respectively
kernel.require(["A", "B"], function(a, b) {
alert(a.foo() + b.foobar()); // displays foofoobar
});
terminology
- service instance: a JavaScript object instance that fulfills some requirement
- service constructor: a JavaScript function that takes zero or more service instances as arguments and returns a new service instance
- service identity: a string that uniquely identifies a service instance within a service container
- service definition: a service constructor paired with an array of zero or more services identities it is dependent on
- service container: a collection of unique service identities, where each service identity is associated with one service instance and, optionally, a service definition that can be used to create the service instance
design goals
- to support the automatic creation of object graphs within a service container through dependency injection, where each object represents a service instance
- to support an AMD define()-like syntax to associate a set of dependencies with a service constructor to create a service definition
- to support an AMD require()-like syntax to retrieve one or more service instances from a service container
- to allow circular dependencies between service definitions, handled in a fashion similar to requirejs.
dependency resolution
in order to fulfill a require() API request, it is necessary for the container to check for the existence of a service instance corresponding to each named service identity specified in the request. If an instance does not yet exist for a given identity, the container obtains the corresponding service definition. Using the service definition, the container recursively tries to get the dependent service instance corresponding to each service identity in the service definition before invoking its service constructor to create the first service instance. Otherwise, an error is raised.
circular dependencies
during the recursive resolution of dependencies for service instances, it may be that a service instance is dependent on another service instance higher in the resolution chain, introducing a circular dependency. As adapted from the requirejs documentation:
“If you define a circular dependency (a needs b and b needs a), then in this case when b‘s [service constructor] is called, it will get an undefined value for a. b can fetch a later after [services] have been defined by using the require() method (be sure to specify require as a dependency so the right context is used to look up a):
// inside b's service definition:
var b = defineService(["require", "a"],
function(require, a) {
//"a" in this case will be null if a also asked for b,
//a circular dependency.
return function(title) {
return require("a").doSomething();
}
}
);
Normally you should not need to use require() to fetch a service instance, but instead rely on the [service instance] being passed in to the function as an argument. Circular dependencies are rare, and usually a sign that you might want to rethink the design. However, sometimes they are needed, and in that case, use require() as specified above.”
the service kernel
we want to be able to do the the following with our dependency injection container:
- define a service with or without dependencies (define())
- define an existing instance as a service (defineInstance())
- undefine an existing service (undefine())
- get one or more service instances (require())
defining the ServiceKernel class
we’ll use a jQuery-like composition to define our class, with private state and methods captured in a closure. For the purposes of exposition, we’ll break the definition of the class into pieces interspersed with commentary. To start with, we’ll provide the copyright declaration:
/* * * A simple JavaScript dependency injection container * By Monroe Thomas http://blog.coolmuse.com * * MIT Licensed. * */
the defineService API
as a basis for defining services, we want a simple function that can be used to bind a list of dependency names (i.e., service identities) to a service constructor function. Strictly speaking, you don’t need to use this function directly, but it is exposed as a public function to make it easier to share a common service definition between multiple containers, since the return value can be passed to ServiceKernel’s define function.
We support three usages of the function:
- defineService(serviceConstructor) – annotates a service constructor with no dependencies
defineService( function() { // return a service instance with a function named "foo" return { foo: function () { return "foo"; } } });
- defineService(dependencyIdentity, serviceConstructor) – annotates a service constructor with one dependency
defineService("A", function(a) { // return a service instance with a function named "foobar", // which is dependent upon some service named A // passed in as parameter a return { foobar: function () { return a.foo() + "bar"; } } });
- defineService(dependencyIdentities, serviceConstructor) – annotates a service constructor with zero or more dependencies
defineService(["A", "B"], function(a, b) { // return a service instance with a function named "baz", // which is dependent upon some services named A and B // passed in as parameters a and b, respectively return { baz: function () { return a.foo() + b.foobar() + "baz"; } } });
here’s the implementation for the defineService function:
/** * Defines a service by annotating a service constructor function with an array of * service identities * @param {Function|String|Array} identitiesOrConstructor * The identities of service dependencies, * or the service constructor if no dependencies exist. * @param {Function} [serviceConstructor] The service constructor. * @return {Function} The annotated service constructor function. */ function defineService(identitiesOrConstructor, serviceConstructor) { if (typeof identitiesOrConstructor === "function") { serviceConstructor = identitiesOrConstructor; if (typeof serviceConstructor.dependencyIdentities !== "undefined") { return serviceConstructor; } identitiesOrConstructor = []; } else if (typeof identitiesOrConstructor === "string") { identitiesOrConstructor = [identitiesOrConstructor]; // wrap in an array } if (!Array.isArray(identitiesOrConstructor)) { throw new Error("identitiesOrConstructor must be an array."); } if (typeof serviceConstructor !== "function") { throw new Error("serviceConstructor must be a function."); } // annotate the constructor with the dependency identity array serviceConstructor.dependencyIdentities = identitiesOrConstructor; return serviceConstructor; }
the ServiceKernel constructor
the rest of the code is defined inside the service kernel constructor function, where we define the private state and functions. For now we’ll stub the private functions and look at their implementations later.
note that we add the kernel itself, as well as its require method, as services available from the container.
/** * Returns a service kernel. * @constructor */ function ServiceKernel() { var instances = {}; // map of named service instances var definitions = {}; // map of name service definitions var beingResolved = {}; // map of instance names that are being resolved var pendingCallbacks = []; // a list of pending require() callbacks function getInstance(identity) { // will be used to get the instance associated with the service identity } function resolveInstance(identity) { // in case that an instance does not yet exist, // this will be used to create the instance associated with the service identity, // recursively resolving any dependencies } function resolvePending(identity, instance) { // when an instance has been resolved, // this will be used to resolve any pending require() callbacks } function PendingCallback(identities, dependencies, pending, callback) { // this is a constructor used to return an object // that can be used to track unresolved dependencies before // a require() callback is invoked } function define(identity, dependencyIdentitiesOrConstructor, serviceConstructor) { // this is the AMD define-like function; // it will be used to define a service with the defineService API } function defineInstance(identity, instance) { // this is a helper function that will be used // to specify an existing instance as a service; } function undefine(identity) { // this will be used to remove a service from the container } function require(identities, callback) { // this is the AMD require-like function; // it will be used to obtain service instances } // create the object that contains just the public methods var kernel = { define: define, defineInstance: defineInstance, undefine : undefine, require : require } // define the kernel itself and its require method as services kernel.defineInstance("kernel", kernel); kernel.defineInstance("require", require.bind(this)); return kernel; }
the define function
the define function is supposed to behave like the AMD-define function. We deviate slightly by requiring the first parameter to name the service; AMD-define specifies that the first module name parameter is optional.
we implement define as a wrapper around the defineService API we implemented earlier; so there are essentially three ways to call define:
var kernel = new ServiceKernel();
// 1. define service named "A" with no dependencies
kernel.define("A",
function() {
// return a service instance with a function named "foo"
return {
foo: function () { return "foo"; }
};
});
// 2. define service named "B", with a dependency on service "A",
// passed in as parameter a to the service constructor
kernel.define("B", "A"
function(a) {
// return a service instance with a function named "foobar"
return {
foobar: function () { return a.foo() + "bar"; }
};
});
// 3. define service named "C", with dependencies on services "A" and "B",
// passed in as parameters a and b to the service constructor
kernel.define("C", ["A", "B"],
function(a, b) {
// return a service instance with a function named "baz",
return {
baz: function () {
return a.foo() + b.foobar() + "baz";
}
};
});
var c = kernel.require("C");
c.baz(); // returns "foofoobarbaz"
here’s the implementation of define:
/** * Defines a service within the kernel. * @param {String} identity The service identity. * @param {Function|String|Array} dependencyIdentitiesOrConstructor * The identities of service dependencies, * or the service constructor if no dependencies exist. * @param {Function} [serviceConstructor] The service constructor. */ function define(identity, dependencyIdentitiesOrConstructor, serviceConstructor) { if (typeof identity !== "string") throw new Error("identity must be a string."); if (identity.length === 0) throw new Error("The identity string may not be empty."); if (identity in definitions) { throw new Error("The service '" + identity + "' has already been defined."); } var definition = defineService(dependencyIdentitiesOrConstructor, serviceConstructor); definitions[identity] = definition; }
the defineIdentity function
the defineIdentity function is a helper to allow the use of an existing object instance as a service. It accomplishes this by creating a service constructor function wrapper that simply returns the instance. This function wrapper is then passed to the define method. Because the service instance is already created, there are no dependencies to specify.
var kernel = new ServiceKernel();
var serviceInstance = {
foo: function() { return "foo"; }
}
kernel.defineInstance("A", serviceInstance);
var a = kernel.require("A");
a === serviceInstance; // returns true
a.foo(); // returns "foo"
here’s the implementation of defineInstance:
/** * Defines a service within the kernel based on an existing instance. * Equivalent to calling define(instance, function() { return instance; }); * @param {String} identity The service identity. * @param {*} instance The service instance. */ function defineInstance(identity, instance) { this.define(identity, function() { return instance; }); }
the undefine function
the undefine function simply removes an existing service definition and instance from a service kernel.
var kernel = new ServiceKernel();
kernel.define("A", function() {
return function() { return "foo"; };
});
var a = kernel.require("A");
a(); // returns "foo"
kernel.undefine("A");
a = kernel.require("A"); // throws an Error
here’s the implementation of undefine:
/** * Undefines a service. * @param {String} identity The service identity. * @return {Boolean} Returns true if the service was undefined; false if it didn't exist. */ function undefine(identity) { if (typeof identity !== "string") throw new Error("identity must be a string."); if (identity in definitions) { delete definitions[identity]; if (identity in instances) { delete instances[identity]; } return true; } return false; }
the require function
the require function is supposed to behave like the AMD-require function. There are three ways to call the require function:
var kernel = new ServiceKernel();
kernel.define("A", function() {
return function() { return "foo"; };
});
kernel.define("B", function() {
return function() { return "bar"; };
});
// 1. with one required service,
// and the service is returned synchronously
var a = kernel.require("A");
a(); // returns "foo"
// 2. with one required service and an async callback
// that is passed the required services as parameters
kernel.require("A", function(a) {
a(); // returns "foo"
});
// 3. with one or more required services and an async callback
// that is passed the required services as parameters
kernel.require(["A", "B"], function(a, b) {
a() + b(); // returns "foobar"
});
here’s an example of using require synchronously as a service in a circular dependency relationship. In the case where A depends on B, and B depends on A (a circular dependency), and a request to the container to resolve B occurs first, then the service reference to B passed into A’s service constructor will be undefined. In order to use service B, A must introduce a dependency on the “require” service, which can be used to resolve the dependency on B when it is needed.
var kernel = new ServiceKernel();
// A depends on B and the require service so it can resolve B as needed
kernel.define("A", ["require", "B"], function(require, b) {
return {
foobar: function() { return "foo" + require("B").bar(); }
};
});
// B depends on A
kernel.define("B", ["A"], function(a) {
return {
foobar: function() { return a.foobar(); }
bar: function() { return "bar"; }
};
});
var b = kernel.require("B");
b.bar(); // returns "bar"
b.foobar(); // returns "foobar"
here’s an example of using require asynchronously as a service in a circular dependency relationship. In the case where A depends on B, and B depends on A (a circular dependency), and a request to the container to resolve B occurs first, then the service reference to B passed into A’s service constructor will be undefined. In order to use service B, A must introduce a dependency on the “require” service, which can be used to resolve the dependency on B asynchronously when the container is finished resolving B.
var kernel = new ServiceKernel();
// A depends on B and the require service so it can resolve B as needed
kernel.define("A", ["require", "B"], function(require, b) {
if (typeof b === "undefined") {
// create an async callback for when B is finally resolved
require("B", function(b2) {
b = b2; // assign the resolved value for future use
});
}
return {
foobar: function() {
if (typeof b === "undefined") {
throw new Error("b not yet resolved");
}
return "foo" + b.bar();
}
};
});
// B depends on A
kernel.define("B", "A", function(a) {
return {
foobar: function() { return a.foobar(); }
bar: function() { return "bar"; }
};
});
var b = kernel.require("B");
b.bar(); // returns "bar"
b.foobar(); // returns "foobar"
here’s the implementation of require:
/** * Returns one or more services. Has similar semantics to the AMD require() method. * @param {String|Array} identities The identities of the services required by the callback. * If callback is not specified, then this must be a string. * @param {Function} [callback] The callback to invoke with the required service instances. * If this is not specified, then identities must be a string, * and the required instance is returned. * @return {*} Returns the specified service instance if no callback is specified; * otherwise returns void. */ function require(identities, callback) { // synchronous version if (typeof callback === "undefined") { if (typeof identities !== "string") { throw new Error("identities must be a string when no callback is specified."); } var instance = getInstance(identities); if (typeof instance === "undefined") { throw new Error("The service '" + identities + "' has not been defined."); } return instance; } if (typeof identities === "string") { identities = [identities]; // wrap in an array } if (!Array.isArray(identities)) throw new Error("identities must be an array."); if (typeof callback !== "function") throw new Error("callback must be a function."); // gather callback arguments var dependencies = []; var pending = 0; for (var i = 0; i < identities.length; i++) { var instance = getInstance(identities[i]); dependencies.push(instance); if (typeof instance === "undefined") { pending++; } } if (pending > 0) { pendingCallbacks.push(PendingCallback(identities, dependencies, pending, callback)); } else { callback.apply({}, dependencies); } }
the PendingCallback constructor function
the PendingCallback function returns an object that manages a pending asynchronous require() callback. The returned object has a resolve method that is called every time a new service instance is resolved. If the newly resolved instance completes all of the pending dependencies for a require() callback, then the callback is invoked.
/** * Returns an object with a resolve function that can be called when a new service instance is created; * the resolve function will return true if all dependencies have been satisfied * and the callback method has been invoked; otherwise it will return false * @param {Array} identities An array of service identity strings. * @param {Array} dependencies An array of dependencies; unresolved dependencies have a value of undefined. * @param {Number} pending The number of unresolved dependencies. * @param {Function} callback The callback to invoke when all dependencies are resolved. * @return {Object} An object containing a resolve function. * @constructor */ function PendingCallback(identities, dependencies, pending, callback) { if (!Array.isArray(identities)) throw new Error("identities must be an array."); if (!Array.isArray(dependencies)) throw new Error("dependencies must be an array."); if (typeof pending !== "number") throw new Error("pending must be a number."); if (typeof callback !== "function") throw new Error("callback must be a function."); if (pending <= 0) throw new Error("pending must be positive."); /** * Checks if the specified service resolves the callback criteria. * @param {String} identity The service identity to resolve. * @param {*} instance The resolved service instance. * @return {Boolean} True if all dependencies are resolved; otherwise false. */ function resolve (identity, instance) { var index = identities.indexOf(identity); if (index === -1) return false; dependencies[index] = instance; if (0 === --pending) { callback.apply({}, dependencies); return true; } return false; } return { resolve : resolve }; }
the getInstance function
the getInstance function is pretty self explanatory:
/** * Returns the service instance corresponding to the specified service identity. * @param {String} identity * @return {*} The service instance; or undefined if the service is being resolved. */ function getInstance(identity) { if (identity in beingResolved) return undefined; if (identity in instances) return instances[identity]; if (identity in definitions) return resolveInstance(identity); throw new Error("The service '" + identity + "' has not been defined.", "identity"); }
the resolveInstance function
the resolveInstance function is the workhorse of the ServiceKernel. It recursively resolves service dependencies and pending require() callbacks that may occur within service constructor functions.
/** * Resolves the service instance corresponding to the specified service identity. * @param {String} identity * @return {*} The service instance. */ function resolveInstance(identity) { if (identity in beingResolved) { // sanity check throw new Error( "resolveInstance is already being called for the service '" + identity + "'."); } var instance; try { beingResolved[identity] = true; var definition = definitions[identity]; // gather the service constructor arguments var dependencies = []; if (definition.dependencyIdentities && Array.isArray(definition.dependencyIdentities)) { for (var i = 0; i < definition.dependencyIdentities.length; i++) { // recursively resolve service dependency; // may be undefined in case of a circular dependency instance = getInstance(definition.dependencyIdentities[i]); dependencies.push(instance); } } // call the service constructor function ConstructorThunk() { return definition.apply(this, arguments[0]); } ConstructorThunk.prototype = definition.prototype; instance = new ConstructorThunk(dependencies); instances[identity] = instance; } finally { delete beingResolved[identity]; } // resolve any pending require calls that may need this instance resolvePending(identity, instance); return instance; }
the resolvePending function
the resolvePending function iterates over the list of pending require() callbacks, checking if the specified service instance completes any required dependencies.
/** * Checks if any pending require callbacks can be completed with the specified service. * @param {String} identity The resolved service identity. * @param {*} instance The resolved service instance. */ function resolvePending(identity, instance) { if (pendingCallbacks.length === 0) return; var resolved, i; for (i = 0; i < pendingCallbacks.length; i++) { if (pendingCallbacks[i].resolve(identity, instance)) { resolved = resolved || []; resolved.push(i); } } // remove any resolved callbacks from the list if (resolved) { for (i = 0; i < resolved.length; i++) { pendingCallbacks.splice(resolved[i], 1); } } }
the end
i hope you find this implementation of a dependency injection container useful… please leave feedback and let me know if you have any problems or suggestions for improvement.
i want to send a shout out to David McFadzean for his review and constructive criticism of this post; it is much improved after his feedback, any remaining problems are entirely my fault.
in case you missed the link near the top of the post, here’s a link to the latest version of the source: https://gist.github.com/4089076
attribution note: the feature photo accompanying this post shamelessly lifted from elearning.industriallogic.com
ciao!
Good stuff Monroe!
One thing that came to mind a few weeks ago was how to use something like a DI framework to provide proxy classes so you might have function-level granularity over how JavaScript is loaded and parsed.
@MattS That sounds interesting! Any more info available on that?