/aodi

Another one Dependency Injection

Primary LanguageJavaScriptMIT LicenseMIT

AODI - Another one Dependency Injection

Build Status Coverage Status Codacy Badge Maintainability codebeat badge devDependencies Status dependencies Status

AODI is a IoC container for JS apps. Works both on browser and Node.JS. Compatible with Flow

Installation

$ npm install aodi --save

Usage

You should create instance of Injector class, provide dependencies for it and create instances of classes, which will ask for this dependencies. Thats it.

// 'real-world' example - part of app config is stored on file system, part saved in database from
// admin dashboard, we want to fetch them and merge in single object which will be used in our app.

import { Injector, Token, inject, injectable } from 'aodi';
import { Database } from 'any-db-you-like';

const ConfigFromFile = new Token();
const ConfigFromDB = new Token();
const AppConfig = new Token();

const injector = new Injector();

injector.provide(ConfigFromFile, {
  factory: async () => fs.readFile(/* fetch config data from file */),
});

injector.provide(Database, {
  factory: async () => Database.createConnection(/* establish connection to DB */),
  singleton: true, // we will use same connection every time instead of creating new one
});

injector.provide(ConfigFromDB, {
  dependencies: [ConfigFromFile, Database], // we need config from file and our DB connection
  factory: async (config, db) => db.fetchDBdata(/*fetch database part of config*/),
});

injector.provide(AppConfig, {
  dependencies: [ConfigFromFile, ConfigFromDB],
  factory: () => ({ ...ConfigFromFile, ...ConfigFromDB }),
});

@injectable
class SomeClassWhichUseConfig {
  @inject(AppConfig) config;

  someMethod(){
    this.config; // any logic which use config
  }
}

injector.provide(SomeClassWhichUseConfig);

@injectable
class App {
  @inject(AppConfig) config;
  @inject(SomeClassWhichUseConfig) myClass;

  anyMethod() {
    this.config;
    this.myClass;
  }
}

injector.provide(App);

async function start() {
  const app = injector.get(App);
}

start();

Of course, in real project you will split this on different files, put class per file, separate providers from classes, which ask for dependency etc, but in general usage can be simple like in example above.

More complex documentation available below:

Table of Contents

DependencyInjection

Provides decorator utilities for injecting dependencies into user classes

inject

Marks property as injectable. This property will be resolved by DI system and injected on instantiating of this class. It will be passed to class constructor as part of key:value oject in first param, so it can be used in constructor. Params for parent class are also included. If class has no constructor at all - resolved dependency will be assigned to class instance by Object.assign().

Parameters

  • dependency (Token<T> | Function) dependency which will be injected to annotated field

Examples

// ES6+ way
import { inject, Token } from 'aodi';

const MyDependencyToken = new Token();
class C{
  @inject(MyDependencyToken) foo;
}
// Flow way
import { inject, Token } from 'aodi';

interface Dep {};

const MyDependencyToken: Token<Dep> = new Token();
class C{
  @inject(MyDependencyToken) foo: Dep;
}
// Example of usage in constructor
import { inject, Token } from 'aodi';

const MyDependencyToken = new Token();
class C{
  @inject(MyDependencyToken) foo;
  constructor({ foo }) {
    // foo is available as a property of params object.
  }
}

Returns Function decorator to annotate class field

injectable

Marks class a injectable. If class use @inject decorators it will be annotated automatically, so it's only mandatory to mark child classes without any @inject to be able to use dependencies provided by parent. You can still mark all classes which uses DI for consistency.

Parameters

  • target Class class to annotate

Examples

import { injectable, inject, Token } from 'aodi';

const MyDependencyToken = new Token();
@injectable //not necessary here, because we already used @inject
class C{
  @inject(MyDependencyToken) foo;
}
import { injectable, inject, Token } from 'aodi';

const MyDependencyToken = new Token();
class Foo {
  @inject(MyDependencyToken) foo;
}

@injectable //necessary, DI should know about parent's params before instantiating Foo
class Bar extends Foo {
  bar() {
    this.foo; //it will be available in child because parent asked this dependency
  }
}

Returns Class annotated class

Token

Class-placeholder, used to bound injectable parameter with some resolvers. It can be used to provide non-class dependencies like numbers or plain objects for example, but it still usefull for classes, which implements same interface, but not inherited from any common base class.

Examples

// ES6+ way
import { Token } from 'aodi';

const MyDependencyToken = new Token();
// flow way
import { Token } from 'aodi';

interface MyDependency {}

// providing interface helps flow to understand to which dependency this token will be resolved
const MyDependencyToken<MyDependency> = new Token();

Injector

Main class, responsible for creating DI container. It purpose to be used at entry point of application and provide dependencies to all classes, constructed inside of container.

provide

Register a simple provider, which resolves one dependency for one token. Syntax is similar to Angular framework.

Parameters

  • token (Token<T> | Function) token for dependency
  • provider Provider? provider which will resolve asked dependency

Examples

// resolves to class instance when someone will ask this class as a dependency
import { Injector } from 'aodi';

class Foo {}

const injector = new Injector();

injector.provide(Foo);
// resolves to instance of other class when someone will ask class or token as a dependency.
import { Injector, Token } from 'aodi';

class Foo {}
class Bar {}

const MyToken = new Token();

const injector = new Injector();

injector.provide(Foo, Bar); //provides Bar when someone will ask for Foo
injector.provide(MyToken, Bar); //provides Bar when someone will ask for MyToken
// resolves to data constructed by factory function
import { Injector, Token } from 'aodi';

const MyToken = new Token();

const injector = new Injector();

injector.provide(MyToken, { factory: () => 42 }); //provides 42 for MyToken
// marks factory as singleton, so it will be calculated only once
import { Injector, Token } from 'aodi';

const MyToken = new Token();

const injector = new Injector();

injector.provide(MyToken, {
  singleton: true,
  factory: () => 42
});
// provide dependencies for factory
import { Injector, Token } from 'aodi';

const MyToken = new Token();
const MyOtherToken = new Token();

const injector = new Injector();

injector.provide(MyToken, { factory: () => 'foo' }); //provides 42 for MyToken
injector.provide(MyOtherToken, {
  dependencies: [MyToken],
  factory: (resolvedToken) => resolvedToken + 'bar';
});
// provide is chainable method
import { Injector } from 'aodi';

class Foo {}
class Bar {}

const injector = new Injector();

injector
  .provide(Foo)
  .provide(Bar);

Returns Injector same injector for chaining purposes

provider

Register a complex provider, based on class, like in java DI libraries. More details at Provider section

Parameters

  • provider Provider class, annotated as provider, which will resolve dependencies

Examples

// register provider
import { Injector, Token, provides } from 'aodi';

const MyToken = new Token();

const injector = new Injector();

class MyProvider {
  @provides foo: MyToken = 'foo'; // MyToken will be resolved to string 'foo';
}

injector.provider(MyProvider);

Returns Injector same injector for chaining purposes

get

Return instance of requested dependency. Dependency should be provided before using injector.get() by calling injector.provide() or injector.provider(). Dependencies without provider can't be resolved. Should be used only at entry point of application, if possible, all classes inside DI container should ask for dependency by @inject annotations. Managing dependencies by yourself inside DI container is an anti-pattern in DI.

Parameters

Examples

// return resolved dependency
import { Injector, Token } from 'aodi';

const MyToken = new Token();

const injector = new Injector();

injector.provide(MyToken, { factory: () => 42 });

// you can call code below inside of async function or use promises
const foo = await injector.get(MyToken); // foo === 42

Returns Promise<T>

create

Create instance of provided class, resolving dependencies for it, but class shouldn't have own provider, like in injector.get(). This method can be usefull when you import class dynamically in runtime. You can also use it inside of your factories. As second argument this method receive object with params to class, which will be merged with resolved dependencies.

Parameters

  • ClassToConstruct Class class to construct with dependency resolve
  • additionalParams Object? object, which will be passed to constructor

Examples

// constructing class
import { Injector, Token, inject } from 'aodi';

const MyToken = new Token();

const injector = new Injector();

injector.provide(MyToken, { factory: () => 42 });

class Foo {
  @inject(MyToken) bar;

  constructor({ foo, bar }) {
    // foo === 24
    // bar === 42
  }
}

// you can call code below inside of async function or use promises
const foo = injector.create(Foo, { foo: 24 });
// using in factories
import { Injector, Token, inject } from 'aodi';

const ConfigFromDB = new Token();

const injector = new Injector();

injector.provide(ConfigFromDB, { factory: async () => fetch(/.../) });

class AppConfig {
  @inject(ConfigFromDB) configFromDB;

  constructor({ configFromDB, ...configFromFile }) {
    // configFromDB resolved as a dependency
    // configFromFile passed from factory below as a parameter
  }
}

const Config = new Token();

injector.provide(Config, { factory: async () => {
  const configData = await loadConfigFromFileHere();
  return injector.create(AppConfig, configData);
}});

// you can call code below inside of async function or use promises
const config = injector.get(Config);

Returns Promise<T> promise, which will be resolved to class instance

Provider

Provides utilities for injecting dependencies and performing IoC

provides

Annotates provider to be responsible for provide dependency for passed DI token. After that DI system will know that this method should be called when any module will ask dependency, associated with this token. It is also possible to annotate class field, in that case value of field will be used as resolved dependency. You can choose any name for method/field, DI does not use it, only annotations and value are important.

Parameters

Examples

// you can provide both value as class field or return it from factory method
import { Injector, Token, provides } from 'aodi';

const MyToken = new Token();
const MyOtherToken = new Token();

const injector = new Injector();

class MyProvider {
  @provides(MyToken)
  foo = 'foo';

  @provides(MyOtherToken)
  bar() {
    return 'bar';
  }
}

// you can call code below inside of async function or use promises
const myToken = injector.get(MyToken); // 'foo'
const myOtherToken = injector.get(MyOtherToken); // 'bar'

Returns Decorator decorator function which can be applied to method or field only

singleton

Annotates provider method as a singleton. Annotated method will be called only once, returned value will be cached and returned on all further method calls.

Parameters

Examples

// you can see that singleton returns same data every time
import { Injector, Token, provides, singleton } from 'aodi';

const MyToken = new Token();

const injector = new Injector();

class MyProvider {
  @provides(MyToken)
  @singleton
  foo() {
    return Symbol('foo');
  }
}

injector.provider(MyProvider);

// you can call code below inside of async function or use promises
await injector.get(MyToken) === await injector.get(MyToken) // true

Returns DescriptorMethod decorated method descriptor

dependencies

Annotates provider method with dependencies list. When this method will be invoked, dependencies list will be resolved by DI mechanism and passed as method parameters.

Parameters

  • dependenciesList ...Token dependencies to inject

Examples

// you can see that singleton returns same data every time
import { Injector, Token, provides, dependencies } from 'aodi';

const MyToken = new Token();
const MyOtherToken = new Token();

const injector = new Injector();

class MyProvider {
  @provides(MyOtherToken)
  foo = 'foo';

  @provides(MyToken)
  @dependencies(MyOtherToken)
  bar(otherToken) {
    return otherToken + 'bar';
  }
}

injector.provider(MyProvider);

// you can call code below inside of async function or use promises
const myToken = await injector.get(MyToken); // 'foobar';

Returns Function decorator function which can be applied to method only

License

MIT