nead
v0.1.0
Published
Dependency injector and container.
Downloads
8
Maintainers
Readme
nead
nead is a powerful but simple tool helping you to code in a more declarative way and to enhance low coupling between your components thanks to dependency injection.
Earnings this package can bring to your code:
- less and dynamic dependencies: more scalability
- identifiable dependencies: more maintainability
- injected dependencies: easier testing (automatic isolation, straightforward validated mocks, ...)
- easier to understand code: more all
nead can be used both client and server sides.
Summary
- Installation
- Use
- Testing
- Contributing
- License
Installation
Run the following command to add the package to your dependencies:
$ npm install --save nead
Use
// CommonJS
const nead = require('nead');
// ES6 modules
import nead from 'nead';
Dependency injector
nead can be used as a simple dependency injector.
Inject a simple dependency
You can inject a dependency to an object thanks to inject
method:
nead.inject = function(object, propertyName, dependency)
Example:
const greeter = {
sayHello: function () {
console.log('Hello world!');
}
};
const program = {
run: function () {
this.greeter.sayHello();
}
};
nead.inject(program, 'greeter', greeter);
program.run() // Display 'Hello world!'.
Validate dependencies
Ok, at this point, some of you wonder what is the benefit of doing that instead of a simple:
const greeter = {
sayHello: function () {
console.log('Hello world!');
}
};
const program = {
run: function () {
greeter.sayHello();
}
};
It is a really long subject that we will not discuss here but you may want to read a bit more about dependency injection
Some others wonder why not just do:
program.greeter = greeter;
And they are right!
For a good injection, you need to check the interface defining the interaction between your 2 objects. And this is where nead can help you! Let's take the previous program
object definition and upgrade it a little:
const program = {
need: {
// Define 'greeter' dependency.
greeter: {
// Define the interface the dependency must implement.
interface: {
methods: ['sayHello']
}
}
},
run: function () {
this.greeter.sayHello();
}
};
When defining method need, you implement the interface needed by nead to check the validity of your dependency:
nead.inject(program, 'greeter', greeter);
// Check that 'greeter' dependency is an object implementing a 'sayHello' method
// (throw an error if it is not the case).
nead.validate(program);
program.run(); // Display 'Hello world!'.
This way you can quickly identify real dependencies between your 2 objects, check their validity during program initialization (i.e. before runtime) and replace your default greeter
object:
import config from 'config';
const greeter = {
sayHello: function () {
console.log('Hello world!');
}
};
const personalGreeter = {
need: {
name: {
value: {type: 'string'}
}
},
sayHello: function () {
console.log(`Hello ${this.name}!`);
}
};
const program = {
need: {
greeter: {
interface: {
methods: ['sayHello']
}
}
},
run: function () {
this.greeter.sayHello();
}
};
if (config.name) {
nead.inject(personalGreeter, 'name', config.name);
nead.inject(program, 'greeter', personalGreeter);
} else {
nead.inject(program, 'greeter', greeter);
}
nead.validate(program);
program.run(); // Display 'Hello world!'.
need property can also be a function in case you would like to evaluate an expression at injection time (like the current time for instance).
Inject many dependencies all at once
You can inject many dependencies in one call:
nead.injectSet = function(object, dependencies) {}
Example:
// ...
import dependency from 'dependency';
nead.injectSet(program, {
greeter,
anotherDependency: 'foo',
anotherObjectDependency: dependency
});
// Validate the dependencies.
nead.validate(program);
You can also inject all dependencies and check in one call using the second argument of injectSet
method. The following is equivalent to the previous example:
// ...
import dependency from 'dependency';
// Inject and validate the dependencies.
nead.injectSet(program, {
greeter,
anotherDependency: 'foo',
anotherObjectDependency: dependency
}, true);
Obfuscate injected property
You can use the property
attribute to obfuscate the access to your dependencies:
const greeter = Symbol('greeter');
class Program {
get need() {
return {
greeter: {
property: greeter,
interface: {
methods: ['sayHello']
}
}
};
}
run() {
this[greeter].sayHello();
}
}
Define dependency getters and setters
You can also define getters and setters in your interface:
class Program {
get need() {
return {
greeter: {
property: greeter,
interface: {
methods: ['sayHello', 'sayBye'],
getters: ['name'],
setters: ['name']
}
}
};
}
// ...
}
Define a value type dependency
As you may have noticed in a previous example, it is possible to define a value type giving a validation schema:
class personalGreeter {
get need() {
return {
name: {
value: {type: 'string'}
}
}
}
// ...
};
Validation schemas are powered by felv.
Define an optional dependency
You can set a dependency as optional:
class Program {
get need() {
return {
greeter: {
property: greeter,
interface: {
methods: ['sayHello']
},
optional: true
}
};
}
// ...
}
Define a "proxified" dependency
In some cases, the real dependency is not directly on the injected object but in the objects provided by this latter.
Factory proxy
factory
proxy helps you to create objects (or immutable values) with validated interface (or value) at runtime.
// A factory must implement 'create' and 'getObjectPrototype' methods.
class GreeterFactory {
create(dependencies) {
return {
sayHello: function () {
console.log(`Hello ${dependencies.name}!`);
}
};
}
getObjectPrototype() {
return create({name: 'foo'});
}
}
class Program {
get need() {
return {
personName: {
value: {type: 'string', required: true}
},
greeterFactory: {
// Allow to check that injected dependency has a `create` method
// and the created objects respect the given interface.
proxy: 'factory',
interface: {
methods: ['sayHello']
}
}
};
}
run() {
const greeter = this.greeterFactory.create({name: this.personName});
greeter.sayHello();
}
}
nead.injectSet(program, {
personName: 'John Doe',
greeterFactory: new GreeterFactory()
});
nead.validate(program);
program.run(); // Display 'Hello John Doe!'.
Registry proxy
registry
proxy provides a way to pass a registry of objects (or immutable values) with validated interface (or value) as dependency.
const greeters = {
friend: {
sayHello: function (firstName, lastName) {
console.log(`Hi ${firstName}!`);
}
},
superior: {
sayHello: function (firstName, lastName) {
console.log(`Good morning Mister ${lastName}.`);
}
}
};
// A registry must implement 'get' and 'getAll' methods.
class GreeterRegistry {
get(key) {
return greeters[key];
}
getAll() {
return greeters;
}
}
class Program {
get need() {
return {
personName: {
value: {type: 'string', required: true}
},
personProximity: {
value: {type: 'string', default: 'superior'}
},
greeterRegistry: {
// Allow to check that injected dependency has a `get` and `getAll` methods
// and the registered objects respect the given interface.
proxy: 'registry',
interface: {
methods: ['sayHello']
}
}
};
}
run() {
const greeter = this.greeterRegistry.get(this.personProximity);
greeter.sayHello(this.firstName, this.lastName);
}
}
nead.injectSet(program, {
firstName: 'John',
lastName: 'Doe',
personProximity: 'friend',
greeterRegistry: new GreeterRegistry()
});
nead.validate(program);
program.run(); // Display 'Hi John!'.
nead.injectSet(program, {
firstName: 'John',
lastName: 'Doe',
personProximity: 'superior',
greeterRegistry: new GreeterRegistry()
});
nead.validate(program);
program.run(); // Display 'Good morning Mister Doe.'.
List proxy
list
proxy provides a way to pass an ordered list of objects (or immutable values) with validated interface (or value) as dependency.
const greeters = [
{
sayHello: function (firstName, lastName) {
console.log(`Hi ${firstName}!`);
}
},
superior: {
sayHello: function (firstName, lastName) {
console.log(`Good morning Mrs. ${lastName}.`);
}
}
];
class Program {
get need() {
return {
personName: {
value: {type: 'string', required: true}
},
greeterIndex: {
value: {type: 'number', default: 0}
},
greeters: {
// Allow to check that injected dependency is an instance of `Array`
// and the item objects respect the given interface.
proxy: 'list',
interface: {
methods: ['sayHello']
}
}
};
}
run() {
const greeter = this.greeters[this.greeterIndex];
greeter.sayHello(this.firstName, this.lastName);
}
}
nead.injectSet(program, {
firstName: 'Jane',
lastName: 'Doe',
greeterIndex: 0,
greeters: []
});
nead.validate(program);
program.run(); // Display 'Hi Jane!'.
nead.injectSet(program, {
firstName: 'Jane',
lastName: 'Doe',
greeterIndex: 1,
greeters: []
});
nead.validate(program);
program.run(); // Display 'Good morning Mrs. Doe.'.
Inject a dependency with an accessor
In some rare cases (external lib service, specific object, ...), you may need to use an accessor to inject a dependency.
In that kind of situation, you can use a ({injectedValue, injectDependency}
) wrapper around your injected dependency:
const originalObject = ['plop'];
const injectedObject = injector.inject(
originalObject,
'items',
{
injectedValue: ['plip', 'plup'],
injectDependency: (object, value) => {
value.forEach(item => object.push(item));
}
}
);
console.log(injectedObject); // ['plop', 'plip', 'plup']
console.log(originalObject); // ['plop']
Use it with caution because it becomes easier to break immutability.
Dependency injection container
Of course, this is a nice thing to inject dependencies like that but it can be a little tedious to:
- instantiate all your services in the correct order
- instantiate helper objects (registries, lists, ...)
- cut your (too big unreadable) injection file
No problem! You can pass to the next level of dependency injection using a dependency injection container!
Instantiate a container
const options = {};
const container = nead.createContainer();
Use factories to create service definitions
You can create service definitions thanks to container create
method:
container.create = function (factoryName, creationName, creationOptions) {}
Arguments:
{string} factoryName
: The service factory to use for creation.{string} creationName
: The name used for service creation(s).{Object} [creationOptions={}]
: The options used for service creations (specific to the factory).
As you can define your own factories, there are some you can already use for main cases.
In following examples, we are going to use previous injection chapter program
and greeter
objects without referring to their implementations in order to focus on the most important part.
Service factory
You can define simple services with service
factory:
container.create('service', 'greeter', {
object: Greeter
});
container.create('service', 'program', {
object: program,
singleton: true,
dependencies: {
// Inject 'greeter' service in 'greeter' need property if exists,
// property descriptor or simple property.
greeter: '#greeter'
}
});
Creation options:
{Object} object
: The service.{boolean} [singleton=false]
: If set to true, use the given service value as is, otherwise usenew
operator on functions andObject.create
on objects.{Object} [dependencies={}]
: The dependencies (keys being simple properties, property descriptors or need property/function returned keys).{Object} [need]
: A need definition to merge with service own need definition.
#greeter
is a reference string to a service that will be resolved at container building. If you want to inject a standard string starting with a#
, escape it with a double##
.
Registry factory
You can define a registry of dependencies thanks to registry
factory:
container.create('service', 'superiorGreeter', {
object: SuperiorGreeter
});
container.create('registry', 'greeterRegistry' {
type: 'greeter',
items: {
friend: FriendGreeter,
family: FamilyGreeter,
superior: '#superiorGreeter',
boss: '#superiorGreeter',
default: {
sayHello: (name) => {
console.log(`hi ${name}!`);
}
}
}
});
This will create a registry service with 5 items (friend
, family
, superior
, boss
, default
) and a superiorGreeter
service.
Creation options:
{string} [type=item]
: The type of item of the registry. Generate default name from service key (e.g.superGreeterRegistry
=>super greeter
).{Object} items
: An object of homogeneous services/values.
Then, you can use your registry like the following:
registry.get('friend').sayHello('John Doe');
Remember that you can check real dependencies with registry items.
List factory
You can define an ordered list of dependencies thanks to list
factory:
container.create('service', 'superiorGreeter', {
object: SuperiorGreeter
});
container.create('list', 'greeterList' {
items: [FriendGreeter, FamilyGreeter, '#superiorGreeter']
});
This will create an ordered list service with 3 items [friend
, family
, superior
] and a superiorGreeter
service.
Creation options:
{Array|Object} items
: An array (or object for unordered list) of homogeneous services/values.
Then, your list is an instance of Array
that you can use like the following:
list[1].sayHello('John Doe');
Remember that you can check real dependencies with list items.
Factory factory
You can define a factory of dependencies thanks to factory
factory:
container.create('factory', 'greeterFactory', {
object: Greeter,
dependencies: {
proximity: 'friend'
}
});
This will create a factory service allowing to instantiate greeter objects and inject them a proximity
dependency of value friend
.
Creation options:
{function|Object} object
: The constructor or prototype.{Object} [dependencies={}]
: The dependencies (keys being simple properties, property descriptors or need property/function returned keys).
Then, you can use your factory like the following:
factory.create({name: 'Bobby'});
Note that you can give dependencies at application initialization in the factory definition (e.g.
proximity
) or at instantiation time (e.g.name
).
Remember that you can check real dependencies with instantiated objects.
Data factory
You can define a structured data dependency thanks to data
factory:
container.create('data', 'config', {
data: {
names: {
myName: 'Jane Doe',
yourName: 'John Doe'
}
},
schema: {
names: {
type: 'object',
properties: {
type: 'string'
}
}
}
});
Creation options:
{Object} data
: The data.{Object} [schema]
: The optional validation schema (powered by felv).
Then, you can inject some part of your data like the following for instance:
container.create('service', 'greeter.me', {
object: Greeter,
dependencies: {
name: '#config.names.myName'
}
});
container.create('service', 'greeter.you', {
object: Greeter,
dependencies: {
name: '#config.names.yourName'
}
});
An example of use case for
data
factory is to define a configuration describing a dynamic execution flow from low coupled components.
Set factory
You can define a set of services thanks to set
factory:
container.create('set', 'greeter', {
items: {
friend: {
object: FriendGreeter,
dependencies: {
name: 'Jane Doe'
}
},
stranger: {
object: strangerGreeter,
singleton: true
}
},
dependencies: {
name: 'John Doe'
},
registry: 'greeterRegistry',
list: 'greeterList'
});
This will create 4 services:
- 2 greeters:
greeter.friend
andgreeter.stranger
- 1 registry:
greeterRegistry
containing the 2 greeters - 1 list:
greeterList
containing the 2 greeters
Creation options:
{Object} items
: An object of items with the same options as forservice
factory. Options will override default ones defined in the definition root.{Object} [object]
: The default service (constructor or prototype for instance).{boolean} [singleton=false]
: The default singleton value. If set to true, use the given service value as is, otherwise usenew
operator on functions andObject.create
on objects.{Object} [dependencies={}]
: The dependencies (keys being simple properties, property descriptors or need property/function returned keys) that will be injected to all created services.{string} [registry]
: An optional registry name to create containing the created services.{string} [list]
: An optional list name to create containing the created services.
Build container
When you are done with all your service definitions, you can build your container to instantiate all your services:
container.build();
Here is the algorithm used during building:
- Sort definitions in instantiation order
- For each definition
- Instantiate service
- Resolve references
- Merge service need with definition need.
- Inject and validate dependencies
- Build service references
Create services from a definition object
Ok, this is correcting previous exposed problems but it is still a bit verbose. Instead of defining each factory independently, you can call many factories at the same time using createSet
method:
container.createSet = function(definitions, build = false) {}
Example:
container.createSet({
// Create a simple `program` service.
program: {
factory: 'service',
object: program,
singleton: true,
dependencies: {
greeterRegistry: '#greeterRegistry'
}
},
// Create a set of `greeter` services with a registry.
greeter: {
factory: 'set',
items: [
{
name: 'friend',
object: FriendGreeter
},
{
name: 'stranger',
object: strangerGreeter,
singleton: true
}
],
dependencies: {
name: '#config.names.myName'
},
registry: 'greeterRegistry',
},
// Create a data `config` service.
config: {
factory: 'data',
data: {
names: {
myName: 'Jane Doe'
}
},
schema: {
names: {
type: 'object',
properties: {
type: 'string'
}
}
}
}
});
container.build();
You can create all definitions and build the container in one call using the second argument of createSet
method:
container.createSet({
// ...
}, true);
Composition
A nice feature in nead is that you can compose your containers thanks to compose
method:
container.compose = function(namespace, container) {}
This is useful in order to use service definitions of dependency packages.
Let's take the code of this third party container for instance:
import nead from 'nead';
export const container = nead.createContainer();
container.createSet({
greeter: {
object: Greeter
},
// ...
});
container.build();
You can use it services in your own one thanks to composition:
import nead from 'nead';
import {container as thirdPartyContainer} from 'thirdParty';
const container = nead.createContainer();
// Compose with my third party container.
container.compose('thirdParty', thirdPartyContainer);
container.createSet({
program: {
object: program,
dependencies: {
// Inject third party greeter as dependency.
greeter: '#thirdParty.greeter'
}
},
// ...
});
container.build();
You can use all third party container services as it would be your own just by prefixing them with the namespace passed to the 'compose' method.
Hacking
This part is not implemented yet.
Here is some tricks to customize your nead experience!
Define your own injection proxy
nead.addInjectionProxy('myInjectionProxy', schema, (options) => {
// ...
});
Define your own service definition factory
nead.addServiceDefinitionFactory('myServiceDefinitionFactory', schema, (options) => {
// ...
});
Testing
Unit tests
Using dependency injection pattern is usually a nice thing for your unit tests (automatic isolation, straightforward validated mocks, ...). Of course, you can use nead to inject your mocks and stubs and easily validate their interfaces:
import nead from 'nead';
import greeter from './greeter';
nead.injectSet(greeter, {
name: 'John Doe',
target: 'friend'
}, true);
// ...
Integration tests
In integration tests you would like to test the interactions between many objects and services.
To make your life easy, you may want to define one (or more) mocked (or not) container(s):
// test/fixtures/container.js
const nead = require('nead');
const Greeter = require('./Greeter');
const container = nead.createContainer();
container.createSet({
greeter: {
object: Greeter
},
// ...
}, true);
module.exports = container;
Then, use it in your tests:
// test/integration/index.js
const container = require('../fixtures/container');
const greeter = container.get('greeter');
// ...
Testing
Many npm
scripts are available to help testing:
$ npm run {script}
check
: lint and check unit and integration testslint
: linttest
: check unit teststest-coverage
: check coverage of unit teststest-debug
: debug unit teststest-integration
: check integration teststest-watch
: work in TDD!
Use npm run check
to check that everything is ok.
Contributing
If you want to contribute, just fork this repository and make a pull request!
Your development must respect these rules:
- fast
- easy
- light
You must keep test coverage at 100%.
License
## TODO
- upgrade validation error tracking (upgrade felv namespace)
- add lib hacking helpers
- add logging and dependency graph
A dependency graph is logged at debug level:
|- program |- greeterRegistry |- greeter.friend |- greeter.family |- strangerGreeter