4.0.0-alpha.0 • Published 3 years ago

iniettore v4.0.0-alpha.0

Weekly downloads
33
License
ISC
Repository
github
Last release
3 years ago

iniettore

Iniettore is a minimalistic Dependency Injection Container for JavaScript and TypeScript.

It's platform agnostic so it can be used on many platform running JS (e.g. Node.js, browsers, React Native).

Install

Using npm:

npm install iniettore --save

Using Yarn:

yarn add iniettore

Basic Concepts

Binding

A binding in an opaque object responsible of managing the lifecycle of one of your instances. Iniettore makes no assumptions about the type or shape of the managed instance.

Not just about objects. In this documentation we will use the term instance with the most generic meaning of the term. In other words you can replace instance with function and everything we describes here still applies.

Out of the box Iniettore provides 2 types of bindings:

  • Provider bindings can be used in any scenario you want a different instances of your type to be "provided" when the corresponding dependency is requested.

  • Singleton bindings can be used in all scenarios you need only one instance of the specified type to exist at any given point in time.

Context

A context is a glorified JS Object containings a collection of bindings. A context helps to structure your application in modular way by letting you group things that have a similiar lifecycle.

Iniettore contexts can be organized in a hierarchy so to accommodate different application composition needs.

Getting Started

We will go over the fundamentals using an example. See below a typical dependency scenario.

interface Logger {
  log(msg: string): void
}

class ConsoleLogger implements Logger {
  log(msg: string) {
    console.log(msg)
  }
}

class HeroService {
  constructor (readonly logger: Logger) {}

  fightBaddie () { /* ... */ }
}

We will instruct Iniettore to create a HeroService instance and provide a ConsoleLogger instance via constructor injection.

Define the Context

You can define a context with one singleton of Logger and a provider of HeroService where the first is a dependency of the latter.

import { container, Context, get, singleton } from 'iniettore'

type CustomContext = Context<{ logger: Logger, hero: HeroService }>

const context: CustomContext = container(() => ({

  logger: singleton(() => new ConsoleLogger())

  hero: provider(() => new HeroService(get(context.logger)))
}))

WARNING provider bindings in Iniettore v4 do behave differently than PROVIDER mappings in pre-v4 versions of Iniettore. See API Reference.

Request instances

Requesting an instance of the HeroService will end up in also creating an instance of the ConsoleLogger which will be provided to the HeroService constructor.

const hero = get(context.hero)

hero instanceof HeroService // true

Two or more instances of the HeroService will share the same instance of the ConsoleLogger.

const hero1 = get(context.hero)
const hero2 = get(context.hero)

hero1 === hero2 // false
hero1.logger === hero2.logger // true

Free memory

When an instance is no longer needed we can tell iniettore via the free handler function.

import { free } from 'iniettore'

const context = ...

let hero = get(context.hero)

// use hero here

hero = null
free(context.hero)

Any internal reference to the ConsoleLogger instance will be deleted so that the instance can be garbage collected.

IMPORTANT: It's developer responsability to notify Iniettore when an instance is no longer used.

API Reference

singleton(materialize [, dispose])

Creates a singleton binding.

Singleton bindings makes no assumptions about the way you create new instances.

Parameters

  • materialize: () => T - a user defined function that returns instances of T
  • dispose: (T) => void (optional) - a function that will be used to run bespoke dispose logic of the given instance.

Returns

BindingDescriptor<T> - an opaque object that describe how to manage the lifecycle of the singleton instance.

Examples

Using a constructor:

import { container, singleton } from 'iniettore'

const context = container(() => ({
  createdAt: singleton(() => new Date())
}))

Using a factory function:

import { container, singleton } from 'iniettore'

function createdAt() {
  return new Date()
}

const context = container(() => ({
  createdAt: singleton(createdAt)
}))

With a bespoke dispose logic:

import { container, singleton } from 'iniettore'

class Car {
  start() { /* ... */ }
  stop() { /* ... */ }
}

const context = container(() => ({
  c: singleton(
    () => new Car(),
    car => car.stop()
  )
}))

provider(materialize)

Creates a provider binding.

Provider bindings are agnostic about the logic you use to provide an instance. This means you can create new instances with a factory function or you can leverage a pool of instances and implement your own reuse logic.

Parameters

  • materialize: () => T - a user defined function that returns instances of T

Returns

BindingDescriptor<T> - an opaque object that describe how to manage the lifecycle of the provider instances.

Examples

Using a constructor:

import { container, provider } from 'iniettore'

const context = container(() => ({
  createdAt: provider(() => new Date())
}))

Using a factory function:

import { container, provider } from 'iniettore'

const context = container(() => ({
  now: provider(Date.now)
}))

container(describe)

Creates a new context with the bindings specified in the describe function.

Parameters

  • describe: () => { [string]: BindingDescriptor<unknown> } - a user defined function that returns an object of BindingDescriptor<T>.

Returns

{ [string]: Binding<BindingDescriptor<unknown>> }

OR more conveniently:

Context<{ [string]: unknown }>

See Context type later in this documentation.

Example

import { container, singleton } from 'iniettore'

const context = container(() => ({
  now: singleton(() => new Date())
}))

get(binding)

Materialize the T associated with the given Binding<BindingDescriptor<T>>. The get function works both on singleton and provider bindings. In fact the get function is agnostic about the lifecycle of the requested instance.

Parameters

  • binding: Binding<BindingDescriptor<T>> -

Returns

T

Example

import { container, get, singleton } from 'iniettore'

const context = container(() => ({
  createdAt: singleton(() => new Date())
}))

const date = get(context.createdAt)

free(bindingOrContext)

Notifies the binding that one of the consumers is no longer using one if its instance(s).

Each singleton binding has an internal counter that allows iniettore to keep track of the number of instances that depends on it. When a singleton binding internal counter reaches zero, the binding clears any internal reference to the instance so it can be garbage collected propertly. A custom dispose logic could also be specified before the references gets cleared (see singleton).

Iniettore is able to figure out what singletons can be freed in the case of simple, direct dependencies (like in the HeroService -> ConsoleLogger example above) and also in more complex scnearios where the web of dependencies involves several "binding hops". This works even in the case some of the bindings in between are not singleton bindings.

Parameters

  • bindingOrContext: Binding<unknown> | Context<{ [string]: unknown }> - a Binding<T> object or an entire Context<T>.

Examples

Using a binding:

import { container, get, singleton } from 'iniettore'

const context = container(() => ({
  createdAt: singleton(() => new Date())
}))

let date = get(context.createdAt)

data = null
free(context.createdAt)

Using a context all the binding within context will be notified and disposed accordingly:

import { container, get, singleton } from 'iniettore'

const context = container(() => ({
  createdAt: singleton(() => new Date())
}))

let date = get(context.createdAt)

data = null
free(context)

Context type

Context is a convenient TS Generic Object type that helps declaring the shape of an Iniettore Context.

In any non-trivial usage of Iniettore, the Context is used to define the shape of an Iniettore Context so to be able to declare dependencies within the context itself (see Getting Started).

Context and ISP

The Interface Segregation Principle (ISP) is one of the five SOLID principles of object-oriented programming. For the purpose of this documentation we will just use a more practical definition of ISP.

The ISP advises us not to depend on classes that have methods we don't use. - Clean Architecture (Robert C. Martin)

As an application grows sometimes it's wise to break it down into application modules that have clear boundaries with the main application. In some Computer Science literatures these modules are referred to as Components depending on how these are deployed/packaged. See an example components diagram that visualize this.

                          ┌──────────────┐
                       ┌──┴─┐            │
                       └──┬─┘            │
                          │     Main     │
                       ┌──┴─┐            │
                       └──┬─┘            │
                          └───────┬──────┘
                                  │
                                  │
           ┌──────────────────────┼───────────────────────┐
           │                      │                       │
           │                      │                       │
   ┌───────┴──────┐       ┌───────┴──────┐        ┌───────┴──────┐
┌──┴─┐            │    ┌──┴─┐            │     ┌──┴─┐            │
└──┬─┘            │    └──┬─┘            │     └──┬─┘            │
   │  Component A │       │  Component B │        │  Component C │
┌──┴─┐            │    ┌──┴─┐            │     ┌──┴─┐            │
└──┬─┘            │    └──┬─┘            │     └──┬─┘            │
   └──────────────┘       └──────────────┘        └──────────────┘

Each component has it's own objects and it's own internal dependency needs.

It is possible to have one single Iniettore Context that defines all dependencies between indidual components objects. As an application grow it might become quite difficult to manage all such composition logic in one place.

One might need to break down the composition logic into a main application Context and one sub-Context per component.

Iniettore facilitate this by letting the developer create different Iniettore contexts where the individual component composition is defined and segregated.

Using the components diagram above and the Logger, ConsoleLogger and HeroService types defined at the beginning we can explain this with an example.

Let's assume that our hypothetical application has some logging constraints that require to have only one Logger object for the entire application. Such instance must be registered in the main Iniettore Context.

main

type MainContext = Context<{ logger: Logger, hero: HeroService }>

const mainContext: MainContext = container(() => ({

  logger: singleton(() => new ConsoleLogger())

  hero: provider(() => new HeroService(get(context.logger)))

  /* other bindings */
}))

Let's now assume that Component A needs to log something. Let's also make clear that Component A does NOT need HeroService.

One can use the Context generic type to isolate the portion of the main Iniettore Context needed by Component A.

RequireLoggerContext

type RequireLoggerContext = Context<{ logger: Logger }>

component-a

function init(main: RequireLoggerContext) {
  const componentContext: CustomContext = container(() => ({
  
    hero: singleton(() => new HeroService(get(main.logger)))
  
    /* other bindings */
  }))

  /* ... */
}

The example above uses an hypothetical init function to initialize the component. This is not meant to prescribe any specific solution. It just helps with explaining how to use Context generic type to follow the ISP.

4.0.0-alpha.6

3 years ago

4.0.0-alpha.0

3 years ago

3.0.0-RC1

5 years ago

3.0.0-alpha-1

7 years ago

3.0.0-pre

7 years ago

2.0.1

9 years ago

2.0.0

9 years ago

2.0.0-pre

10 years ago

1.2.0

10 years ago

1.1.0

10 years ago

1.0.0

10 years ago

0.0.5

10 years ago

0.0.4

10 years ago

0.0.3

10 years ago

0.0.2

10 years ago

0.0.1

10 years ago