jerkface
v1.0.0
Published
Hey, look, it's a dependency injection container for Node.js.
Downloads
27
Maintainers
Readme
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 -S
The 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 theContainer
class.errors
: an object with the following keys:BindingError
: a reference to theBindingError
class.CircularReferenceError
: a reference to theCircularReferenceError
class.ResolveError
: a reference ot theResolveError
class.
Lifetime
: a reference to theLifetime
enum.
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 ofContainer
across an application. This property is by defaultnull
, and will throw aTypeError
if you attempt to set it to anything other thannull
or an instance ofContainer
.
Method
Container.prototype.bind(name, target [, options])
: bindsname
totarget
.Dependencies do not have to be bound in a specific order. The
jerkface
module 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, aCircularReferenceError
is 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 whichname
is bound. This can be either a constructor function, or any type. In the event a constructor function is provided, it is new'ed up whenname
is resolved.options
: (optional) an object that can be provided to further customize howjerkface
treats a binding. Keys include:dependencies
: (optional) an object where each key maps to the name of a key on thedependencies
object argument on a bound constructor. See Bound Constructors for more information. Iftarget
is not a constructor function then setting thedependencies
key will result in aBindingError
being 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"
. Iflifetime
is set to"transient"
, then an instance oftarget
is new'ed up every timeresolve()
is called. Iflifetime
is set to"singleton"
, which is the default, then a single instance is created. If thetarget
argument is not a constructor function, and this option is set, thenBindingError
is 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 atarget
constructor function is derived frombase
with additional dependencies.For example, lets assume
ClassA
extendsClassB
, andClassA
is bound to the namea
.ClassA
also has a dependency to a binding namedfoo
.ClassB
is configured usingbindAll()
, and specifies the bindingbar
as a dependency. When the bindinga
is resolved,ClassA
will be new'ed with bothfoo
andbar
in itsdependencies
map.If there are any key collisions on dependencies defined by
Container.prototype.bindAll()
betweenbase
and any bindings that extend frombase
, those defined at the binding level are preferred.Further, if there are multiple
base
objects defined withContainer.prototype.bindAll()
, thenjerkface
will 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 thedependencies
object 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 samename
return the same instance.If the binding specified by
name
has a dependency that has not been configured, thenResolveError
is 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
// 3
Class: 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.