Created
November 16, 2012 17:12
-
-
Save mmthomas/4089076 to your computer and use it in GitHub Desktop.
A simple JavaScript dependency injection container
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* 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