inversio
A Promise-based dependency injection system.
Motivation
- Clean and intuitive way of organizing large applications into truly separate modules
- A cure for require-hell
The basics
Make sure you have a version of node with a native (or shim) Promise implementation. Otherwise, just install bluebird.
Create a container
var container =
A container is repository of components. It keeps track of their names, dependencies and how and when they are instantiated.
Register a component
container
name
must be unique (unless timid or tags are specified) and can be used by dependant components to reference the component. Anonymous components are allowed if tags are specified (as in{tags: ['a'], factory: () => {...}}
).
factory
is a function that returns a service instance or a promised service instance.
Important: the factory is invoked only once during the container lifetime, effectively making components singletons.
Register dependent components
container
Note that dependecies to other components are explitly listed in
depends
and that the factories have a corresponding argument list.
Resolving components
resolve(name) is used for resolving a single component. inject(...names, optional factory) is used for resolving a multiple components at once.
resolve() and inject() both returns promises, but evaluation of dependencies and invokation of factories will be with settled values - using natural, synchronous composition.
container <!-- resolve single component container <!-- resolve multiple components container <!-- resolve multiple components container <!-- value will be container <!-- value will be
Timid components
A timid component must be named and may be overwritten by another component with the same name, unless already resolved.
container
Special dependencies
? (optional)
Dependencies on the form ?foo
are resolved normally with name (foo
) if registered, otherwise undefined
.
container // => undefined container // => 'A'
require
Dependencies on the form 'require:foo'
are resolved with require('foo')
rather than looking up a component.
container // => require('fs')
tag
Dependencies on the form 'tag:bar'
are resolved with all components tagged with 'bar'
. The resolved result is an array of resolved dependencies.
Tagged components can be anonymous, i.e. name must not be specified.
Tagging is straightforward:
container
Tagging is powerful, since it allows dependencies on the form is a rather than is.
Order of tagged components can be specified with order
:
container
Decorators, mixins and subclassing
The decorator pattern and the particular case of subclassing/mixins are expressed using (replace <name> below with something in your liking)
- a named root (such as a base class) named
'super:<name>'
- decorators (or mixins) named
'extends:<name>'
- a concrete binding named
'class:<name>'
which combines super: and extends: in a natural way.
Decorators are expected to map from one value to another.
Consider the following composition:
console
This can be expressed and evaluated with
container // --> console.log(foo(bar('X')))
The motivation for ___super:___, extends: and class: comes from real world applications where class composition like class A extends (class B extends ... extends SomeBaseClass)
is achieved using the mechanisms above.
Organizing express applications
For a working example, check out the sample-express-site
- concerns are separated into separate modules (database, index page, blog pages, express and middleware etc)
- each module is self contained, including views and controllers
- module dependencies are injected, so there are no
require()
between modules
The challenge (at least for me) with larger express/koa applications is that dependencies are hard to maintain while still handling tests, structure, refactoring, technical debt and such. My personal preference is to separate the application into modules, each with a specific and isolated concern. These modules are then bootstrapped in the main script of the application.
An important principle when working with dependency inversion, is that code that deals with concrete dependency decisions (i.e. which module or class to instantiate) should be kept at a minimum. In the example below, main.js knows about the actual application structure, while the modules are blissfully ignorant.
The main script, where modules are discovered, registered and composed to an aggregate that is your final application.
inject(['app', 'tag:route'], ...)
does some magic. First, it will get the registeredapp
service, needed forlisten()
. But it ill also trigger loading of all modules tagged withroute
, ensuring that the admin, users and blogs routes will be registered with express.The tag syntax for resolving dependencies is vey powerful for ensuring instantiation of service by category without having to know them explicitly.
Globbing for modules is quite cool (in my opinion). It basically allows you just drop in a new module. If tagged as routes, they will be part of your express application.
The express module (./app/express/component.js) could be something like
moduleexports { return container} { var app = // Setup require middleware // app.use(...) return app}
The users/admin/blogs module could be something like
moduleexports { return container} { // setup routes // app.get(...) // app.post(...)}
The database logic could be isolated as
moduleexports { return container} { return './data'}
Having scattered
component.js
files, each exporting a functionregister
for its module registrations, is only a convention used in this example. Other ways of discovering and bootstrapping modules are certainly possible.