jerkface v1.0.0
jerkface
I swore to myself I'd never do this. I don't know how it even happened. It kind of snuck up on me, and before I even realized what was happening I had written a dependency injection library for Node.js. Now here it is. It's fairly opinionated. It suits my purposes, and perhaps it will fit your use case. If not, don't worry, there's a bazillion other DI libraries out there all doing it a different way.
Usage
Add jerkface to your project. Or don't.
$ npm install jerkface -SThe jerkface DI library is primarily intended to work with constructible objects. The idea is that you design your app around application boundaries. Each boundary is represented by an abstraction. Each module in your application is defined as a class whose constructor accepts a parameter that contains all of the class' dependencies.
An example:
const elv = require('elv');
class EventStream {
constructor(dependencies) {
this.driver = elv.coalesce(dependencies.driver, () => new Driver());
}
write(message) {
// use this.driver to write a message to the event stream
}
}
class Storage {
constructor(dependencies) {
this.fs = dependencies.fs;
}
save(file) {
// execute some business logic, and use this.fs to persist the file
}
}
class Uploads {
constructor(dependencies) {
this.eventStream = dependencies.eventStream;
this.storage = dependencies.storage;
}
save(file) {
const self = this;
// assume everything uses promises
this.storage.save(file)
.tap((info) => {
return self.eventStream.write({
topic: '/uploads/file/saved',
payload: info,
});
});
}
}This design has a number of advantages over relying on CommonJS. Not the least of which is that it makes unit testing extremely easy; as, dependencies can be very easily mocked out, and injected.
This has the beginnings of a very basic service-oriented-architecture. Each class in the example above is basically a service. It's fairly common that instances of services are managed as singletons in applications. However, ensuring there is a single instance of each service in an app, and that each module can efficiently access that lone instance is the tricky part.
That's where jerkface comes in. Simply configure each service as though it were a separate package in your app, and define its dependencies.
// When your application starts up:
const Container = require('jerkface').Container;
Container.shared = new jerkface.Container();event-stream.js
// This class manages its own dependencies outside of jerkface, but can still be
// declared as a binding that is injectable into other services.
Container.shared.bind('event-stream', EventStream);storage.js
// Bindings do not have to be a constructor. If a binding is an object, that
// instance will be returned.
Container.shared.bind('fs', require('fs'));
Container.shared.bind('storage', EventStream, {
dependencies: { fs: 'fs' },
});uploads.js
// Tie it all together:
Container.shared.bind('uploads', Uploads, {
dependencies: {
eventStream: 'event-stream',
storage: 'storage',
},
});Now when you want instance of Uploads:
const uploads = Container.shared.resolve('uploads');This method can be called over and over again, from multiple different modules, and you will always get the exactly same instance (there are caveats to this related to how various CommonJS implementations, or whatever module system you're using, handles different versions of packages).
Compatibility
The jerkface module was built with Node.js in mind, though it may work in the browser just fine (as long as you are using a CommonJS module system). I have not gotten around to testing this, so use it in the browser at your own risk.
It's also worth nothing that jerkface was built using ECMAScript 2015, and, so, will require a Node.js version and configuration capable of using class, Map, Set, for...of, etc.
API
The base module is an object with the following keys:
Container: a reference to theContainerclass.errors: an object with the following keys:BindingError: a reference to theBindingErrorclass.CircularReferenceError: a reference to theCircularReferenceErrorclass.ResolveError: a reference ot theResolveErrorclass.
Lifetime: a reference to theLifetimeenum.
Class: Container
The heart of the jerkface project. Each Container functions independently from one another. All bindings and object instances are not shared across Container instances.
Properties
Container.shared: a static, convenience property for sharing an instance ofContaineracross an application. This property is by defaultnull, and will throw aTypeErrorif you attempt to set it to anything other thannullor an instance ofContainer.
Method
Container.prototype.bind(name, target [, options]): bindsnametotarget.Dependencies do not have to be bound in a specific order. The
jerkfacemodule only requires that the entire dependency graph be configured beforeContainer.prototype.resolve()is called.Additionally, the
Container.prototype.bind()method will traverse the dependency graph, and ensure that no circular references are created. If a circular dependency is detected, aCircularReferenceErroris thrown.Subsequent calls to
Container.prototype.bind()with an existingname, will overwrite the previously configured binding. However, any already constructed and resolved dependent bindings will not be modified.This method returns its instance of
Container. This allows multiple calls to be easily chained together.Parameters
name: (required) the name by which this binding is know. All other bindings, will reference this name when declaring dependencies to other bindings.target: (required) the object to whichnameis bound. This can be either a constructor function, or any type. In the event a constructor function is provided, it is new'ed up whennameis resolved.options: (optional) an object that can be provided to further customize howjerkfacetreats a binding. Keys include:dependencies: (optional) an object where each key maps to the name of a key on thedependenciesobject argument on a bound constructor. See Bound Constructors for more information. Iftargetis not a constructor function then setting thedependencieskey will result in aBindingErrorbeing thrown. This key defaults tonull.lifetime: (optional) how the lifetime of constructed object is to be managed. This value is always a string, and can be either"singleton"or"transient". Iflifetimeis set to"transient", then an instance oftargetis new'ed up every timeresolve()is called. Iflifetimeis set to"singleton", which is the default, then a single instance is created. If thetargetargument is not a constructor function, and this option is set, thenBindingErroris thrown.params: (optional) an array of additional parameters expected by the constructor function specified bytarget. See Bound Constructors for more information.
Container.prototype.bindAll(base, dependencies): supplements all bindings where atargetconstructor function is derived frombasewith additional dependencies.For example, lets assume
ClassAextendsClassB, andClassAis bound to the namea.ClassAalso has a dependency to a binding namedfoo.ClassBis configured usingbindAll(), and specifies the bindingbaras a dependency. When the bindingais resolved,ClassAwill be new'ed with bothfooandbarin itsdependenciesmap.If there are any key collisions on dependencies defined by
Container.prototype.bindAll()betweenbaseand any bindings that extend frombase, those defined at the binding level are preferred.Further, if there are multiple
baseobjects defined withContainer.prototype.bindAll(), thenjerkfacewill build out the inheritence order. In the event that any depenency keys collide in the inheritence chain, preference is given to derived classes.Subsequent calls to
Container.prototype.bindAll()with an existingbase, will overwrite the previously configured binding. However, any already constructed and resolved dependent bindings will not be modified.This method returns its instance of
Container. This allows multiple calls to be easily chained together.Parameters
base: (required) the target super class.dependencies: (required) an object where each key maps to the name of a key on thedependenciesobject argument on a bound constructor. See Bound Constructors for more information.
Container.prototype.resolve(name): returns an instance of the object bound toname. If the bound object is a constructor function, it is invoked with all configured parameters and dependencies, and returned.If the binding is managed as a singleton, it is lazily constructed when
resolve()is called, and all subsequent calls toresolve()for the samenamereturn the same instance.If the binding specified by
namehas a dependency that has not been configured, thenResolveErroris thrown.Parameters
name: (required) a string that specifies the name of the binding to resolve.
Bound Constructors
When a target constructor is added as a binding using jerkface, it is important to keep in mind how the Container class injects arguments into the function. This is largely dictated by the options provided to Container.prototype.bind().
When options.dependencies is provided, an object with identical keys is provided to the constructor. However, each key value is replaced by the resolved binding name.
When options.params is provided, the constructor is called with each item in the array provided as a separate argument. In this scenario, if the binding was configured with options.dependencies, then the dependencies object will be the last argument provided to the constructor.
Example
const Container = require('jerkface').Container;
const container = new Container();
class Test {
constructor(a, b, dependencies) {
console.log(a);
console.log(b);
console.log(dependencies.c);
}
}
container.bind('test', Test, {
dependencies: {
c: 'c',
},
params: [1, 2],
});
container.bind('c', 3);
const test = container.resolve('test');
// Written to the console:
// 1
// 2
// 3Class: BindingError
Thrown when options provided to the Container.prototype.bind() method are invalid.
This class is extends the builtin Error class.
Class: CircularReferenceError
Thrown when attempting to create a binding with a dependency that has a dependency to the original binding. Calling Container.prototype.bind() will cause jerkface to walk the entire dependency chain, and CircularReferenceError will be thrown even if the dependency is several times removed from the original binding.
For example, a depends on b, and b depends on c. If either b or c depends on a, then a circular reference is created, and an error is thrown.
This class is extends the builtin Error class.
Class: ResolveError
Thrown when Container.prototype.resolve() is called on a binding that does not exist in the Container instance.
Enum: Lifetime
An enum value that specifies the possible lifetime values that can be used with jerkface:
Lifetime.singleton: instances are managed as singletons.Lifetime.transient: the lifetime of instances are not managed byjerkface. Once an instance is returned byContainer.prototype.resolve()it is entirely up to the consuming code how it lives on past the calling scope.
Performance Considerations
Calling Container.prototype.bind() and Container.prototype.bindAll() should not be called in hot code paths, as the extensive validation routines that must be run are computationally expensive (especially the later). Generally, these methods should only be called during application initialization.
9 years ago