@dflowng/di

0.1.1 • Public • Published

@dflowng/di - DI container and service locator for the Data Flow eNGine

This package is a part of the Data Flow eNGine project.

Usage

Getting root container

To get the root container just import @dflowng/di package:

const DI = require('@dflowng/di');

It also would be useful to call #configure(Module) method. This method will look for package.json of the current module and load all the dependencies (using Module#require(id) call):

DI.configure(module);

If any of packages current package depends on will define any services then they will be available after call to #configure(Module). There also is a version of #configure(Module) method called #configureDev(Module). This method will load dependencies listed in "devDependencies" section of package.json too:

DI.configureDev(module);

Object lifecycle management

There is a class (Referenced) of objects that have a reference counter. This class is defined in @dflowng/di package as there is a dependency loop between DI container and this class.

Methods Referenced#retain() and Referenced#release() increment and decrement a internal reference counter of the object. When after call of Referenced#release() method the reference counter reaches zero (or negative value) the Referenced#destructor() method (destructor) is called and if after destructor execution the reference counter remains non-positive number then the object is recognized as not-alive (the property Referenced#isAlive tells if object is alive).

Methods Referenced#referTo(obj) and Referenced#noReferTo(obj) register and destroy a references from the object to another. When a Referenced#referTo(obj) method is called with object that is instance of Referenced then the reference counter of that object is incremented and reference to that object is saved. When destructor of a object is called all of the references (and probably referred objects) are destroyed.

Method Referenced#do(callback) performs (possibly asynchronous) operation keeping the object alive.

Creating child containers

Child (local) container of a DI container is a container that (at any moment of time) has defined all services defined in it's parent container. Services defined in child container will override the services defined in it's parent.

There are two ways to get a child container. The first one is to use #for(obj) method of parent container:

const x = {};
const di = DI.for(x);

Every call to #for(obj) method will return the same object for the same argument if the argument is a object (even if called on different containers).

If the object passed as argument to #for(obj) is instance of Referenced then the child container becomes able to store services that are instances of Referenced until the object is destroyed.

The second way is to use #withLocal(callback) method:

DI.withLocal(di => {
    // . . .
});

The created container becomes able to store services that are instances of Referenced until the (possibly asynchronous) operation represented by callback is completed.

Defining and resolving dependencies (services)

There are different methods to access different kinds of services -- variables, classes, functions and arrays. Services of any kind have represented by sequence of one or more string identifiers.

Defining and resolving classes

To access class service #class(className[, implementationName1[, ...]]) method should be used:

// Use #get() method to get a raw class (for inheritance).
// There also is #getClass(className[, implName1[, ...]])
// method that is a shorthand for #class(...).get().
class A extends DI.class('Referenced').get() {
    constructor(n) {
        // . . .
    }
    // . . .
};

// Use this if no instances of the class should be created
//A[DI.abstract] = true;

// Use #define(clazz) method to register class
DI.class('A').define(A);

// Method #new(...args) creates new instance and performs
// dependency injection (if necessarry)
const a = DI.class('A').new(42);

Defining and resolving functions

To access function services #func(functionName[, implementationName1[, ...]]) method should be used:

// Use #define(func) to define function:
DI.func('foo').define(function foo(x) {return x*x;});

// The service accessor function will delegate call to the function if it is defined:
DI.func('foo')(2); // Will call foo(2)

// Use #get() method to get the function itself:
DI.func('foo').get() === foo // true

Defining and resolving variables

Class and function services may be defined only as es6-classes and functions. To define services that are not functions or classes there are variable services.

Method #var(varName[, implName[, ...]]) is used to access variable services:

// Set a variable using #set(value) method:
DI.var('bar').set('baz');

// Get a variable using #get() method:
DI.var('bar').get() // 'baz'

Defining and resolving arrays

A special case of service is a array of objects items of which may be distributed between multiple containers (the container and chain of it's parents).

Method #var(varName[, implName[, ...]]) is used to access variable services:

// Define array items in root container:
DI.array('myArray').add('foo','bar');

DI.withLocal(di => {
    // Add items in local container:
    di.array('myArray').add('baz');
    
    // Get array from root and from local containers:`
    di.array('myArray').get() // ['foo', 'bar', 'baz']
    DI.array('myArray').get() // ['foo', 'bar']
});

Dependency injection

Dependency injection is implemented interface-injection-like way. A class instances of which require dependency injection should implement #[DI.injector](di[, ...args]) method. That method will be called by method #new([... args]) of DI container with container the #new([... args]) method was called on as first argument and arguments passed to the #new([... args]) method as the following arguments.

If more than one class in inheritance chain defines #[DI.injector](di[, ...args]) method then all the methods will be called in the same order as constructors of that classes -- that is not necessarry (and even dangerous) to call #[DI.injector](di[, ...args]) of superclass explicitly.

Here is an example of dependency injection:

class A {
    [DI.injector](di) {
        this._n = di.var('n').get();
    }
    
    get n() {
        return this._n;
    }
}

A[DI.abstract] = true;

DI.class('A').define(A);

class A1 extends DI.getClass('A') {
    [DI.injector](di) {
        // [DI.injector] of A is already executed so it's safe to access #n
        this._m = this.n * 2;
    }
    
    get m() {
        return this._m;
    }
}

DI.class('A', '1').define(A1);

DI.withLocal(di => {
    // Will cause error - class A is abstract
    // di.class('A').new()
    
    // Define cariable in local container
    di.var('n').define(42);
    // Create instance of A1 injecting services from local container (di)
    const a = di.class('A', '1').new();
    
    a.m; // 84
});

Readme

Keywords

none

Package Sidebar

Install

npm i @dflowng/di

Weekly Downloads

0

Version

0.1.1

License

MIT

Last publish

Collaborators

  • dflowng