IoC implementation

The IOC implementation is bundled in the NPM package @tmorin/ceb-inversion-core.

This IoC implementation relies on three main concepts. The first one is the registry. Its purpose is to provide services to register and resolve items. The second concept, the module, interacts with the registry in order to configure items to register. Finally, the last one, the container, manages the lifecycle of the modules and, therefore, the lifecycle of the registry's items too.

The Container, the Registry and the Modules

The Container

The Container lifecycle

A Container is created using the ContainerBuilder, and especially its build() method. Once built, the Container can be initialized, so that Modules and Components will be configured. When a Container becomes useless, it has to be disposed, so that Modules and Components will be disposed too. Once disposed, a container cannot be used anymore.

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"

ContainerBuilder.get()
.build() // from `void` to `Built`
.initialize() // from `Built` to `Alive`
.then((container) => {
  return container.dispose() // from `Alive` to `Disposed`
})

The Registry

The Registry handles only two kinds of registration modes: by value and by factory.

With the by value mode, the value must exist at the time of the registration, and the registry will always resolve it.

import { DefaultRegistry } from "@tmorin/ceb-inversion-core"

// create a fresh registry
const registry = new DefaultRegistry()

// register a Value
registry.registerValue("key", "the value")

// resolve the entry
const a = registry.resolve("key")

// resolve again the entry
const b = registry.resolve("key")

// assert instances are the same
console.assert(a === b)

However, with the by factory mode, the value is created at the resolution time. Moreover, its creation can be based on entries available in the registry. By default, the factory is invoked for each resolution.

import { DefaultRegistry } from "@tmorin/ceb-inversion-core"

// create a fresh registry
const registry = new DefaultRegistry()

// register a Factory
registry.registerFactory("key", () => ({ k: "v" }))

// resolve the entry
const a = registry.resolve("key")

// resolve again the entry
const b = registry.resolve("key")

// assert instances are not the same
console.assert(a !== b)

The option singleton prevents this invocation of the factory at each resolution. That means the value is created only once, at its first resolution.

import { DefaultRegistry } from "@tmorin/ceb-inversion-core"

// create a fresh registry
const registry = new DefaultRegistry()

// register a Factory acting as a singleton
registry.registerFactory("key", () => ({ k: "v" }), { singleton: true })

// resolve the entry
const a = registry.resolve("key")

// resolve again the entry
const b = registry.resolve("key")

// assert instances are the same
console.assert(a === b)

The Modules

The modules hierarchy

A Module helps to bundle entry registrations to the registry. It's an implementation of the interface Module. However, a convenient abstracted implementation, AbstractModule, handles already the boilerplate stuff.

import { AbstractModule } from "@tmorin/ceb-inversion-core"

export class WorldModule extends AbstractModule {
async configure(): Promise<void> {
  // register a name
  this.registry.registerValue("name", "World")
}
}

export class GreetingModule extends AbstractModule {
async configure(): Promise<void> {
  // register a factory
  this.registry.registerFactory(
    "greeting",
    (registry) => `Hello, ${registry.resolve("name")}!`
  )
}
}

The modules are used during the creation of a container and must be provided to the ContainerBuilder.

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"
import { GreetingModule, WorldModule } from "./ioc-container-module-class"

ContainerBuilder.get()
.module(new GreetingModule())
.module(new WorldModule())
.build()
.initialize()
.then((container) => {
  // resolve the greeting entry
  const greeting = container.registry.resolve("greeting")
  // assert the greeting text match the expected one
  console.assert(greeting === "Hello, World!")
  // release the stateful stuff
  return container.dispose()
})
.catch((e) => console.error(e))

Sometime, the class usage can be a bit too much. Therefore, an inline way to available using OnlyConfigureModule.create(...).

import { ContainerBuilder, ModuleBuilder } from "@tmorin/ceb-inversion-core"

ContainerBuilder.get()
.module(
  ModuleBuilder.get()
    .configure(function (registry) {
      // register a name
      registry.registerValue("name", "John Doe")
      // register a factory
      registry.registerFactory(
        "greeting",
        (registry) => `Hello, ${registry.resolve("name")}!`
      )
    })
    .build()
)
.build()
.initialize()
.then((container) => {
  // resolve the greeting entry
  const greeting = container.registry.resolve("greeting")
  // assert the greeting text match the expected one
  console.assert(greeting === "Hello, John Doe!")
  // release the stateful stuff
  return container.dispose()
})
.catch((e) => console.error(e))

The Components

The Component

Components follow the lifecycle of Modules, i.e. configure then dispose. To be discovered and managed by the Container, they must be registered with the ComponentSymbol Registry Key.

Components are the right places to implement Process Manager or other reactive programing artifacts.

import { Component } from "@tmorin/ceb-inversion-core"

// define a component
export class MyModule extends Component {
async configure(): Promise<void> {
  // execute things when container starts
}

async dispose(): Promise<void> {
  // execute things when container stops
}
}