A simple JavaScript dependency injection container
/* | |
* | |
* A simple JavaScript dependency injection container | |
* By Monroe Thomas http://blog.coolmuse.com | |
* | |
* http://blog.coolmuse.com/2012/11/11/a-simple-javascript-dependency-injection-container/ | |
* | |
* MIT Licensed. | |
* | |
* Unit tests can be found at https://gist.github.com/4270523 | |
* | |
*/ | |
/** | |
* 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; | |
} | |
/** | |
* Returns a service kernel. | |
* @constructor | |
*/ | |
function ServiceKernel() { | |
var instances = {}; | |
var definitions = {}; | |
var beingResolved = {}; | |
var pendingCallbacks = []; | |
/** | |
* 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"); | |
} | |
/** | |
* Resolves the service instance corresponding to the specified service identity. | |
* @param {String} identity | |
* @return {*} The service instance. | |
*/ | |
function resolveInstance(identity) { | |
if (identity in beingResolved) { | |
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; | |
} | |
/** | |
* 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); | |
} | |
} | |
if (resolved) { | |
for (i = 0; i < resolved.length; i++) { | |
pendingCallbacks.splice(resolved[i], 1); | |
} | |
} | |
} | |
/** | |
* 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 { | |
/** | |
* 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. | |
*/ | |
resolve : resolve | |
} | |
} | |
/** | |
* 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; | |
} | |
/** | |
* 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; }); | |
} | |
/** | |
* Undefines a service. | |
* @param {String} identity The service identity. | |
* @return {Boolean} Returns true if the service was undefined; false otherwise. | |
*/ | |
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; | |
} | |
/** | |
* 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); | |
} | |
} | |
// create the object that contains the kernel methods | |
var kernel = { | |
/** | |
* 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. | |
*/ | |
define: define, | |
/** | |
* 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. | |
*/ | |
defineInstance: defineInstance, | |
/** | |
* Undefines a service. | |
* @param {String} identity The service identity. | |
* @return {Boolean} Returns true if the service was undefined; false otherwise. | |
*/ | |
undefine : undefine, | |
/** | |
* 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. | |
*/ | |
require : require | |
} | |
// define the kernel itself and its require method as services | |
kernel.defineInstance("kernel", kernel); | |
kernel.defineInstance("require", require.bind(this)); | |
return kernel; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment