Skip to content

Dependency Injection

Miro Spönemann edited this page Sep 13, 2019 · 2 revisions

Sprotty uses InversifyJS to configure the various components of the client using dependency injection (DI). DI allows us to

  • not care about the instantiation and life-cycle of service components,
  • manage singletons like the various registries without using the global scope,
  • easily mock components in tests,
  • exchange default implementations with custom ones with minimum code changes,
  • modularize the configuration of specific features and scenarios and merge these modules for the final application.

DI Client Side

Example: A class needs a logger, so it has it injected via DI using @inject:

class Foo {
   @inject(TYPES.ILogger) public logger: ILogger
   ...
}

Notes:

  • We always use symbols like TYPES.ILogger for bindings to avoid cycles in JavaScript's import resolution. We had a couple of weird issues when we were using classes directly which are pretty hard to track down. By putting all binding symbols in a common file and namespace, we can avoid this.
  • When using symbols, Typescript requires us to explicitly state the type.
  • We currently don't care whether we have dependencies injected as arguments to the constructor or as default values in the fields, as InversifyJS requires us to resolve cyclic dependencies using provider bindings anyway.

Multi-bindings

Sometimes there is more than one implementation bound to a specific interface. This is when we use multi-bindings. Here is an example for the VNodeDecorators.

@multiInject(TYPES.VNodePostprocessor)@optional() protected postprocessors: VNodePostprocessor[]

Notes:

  • @optional avoids a runtime error when nothing is bound to the symbol.

DI Provider Side

InversifyJS requires us to mark classes as @injectable.

Configuration

We use ContainerModules from InversifyJS to describe the bindings. Each feature defines its own module describing the components it requires. For the entire application, a number of modules are merged, e.g.:

const container = new Container()
container.load(defaultModule, selectModule, moveModule, boundsModule, viewportModule, flowModule)

A module can rebind symbols from prior modules:

const flowModule = new ContainerModule((bind, unbind, isBound, rebind) => {
    rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope()
    ...
}

Singleton Bindings

We use a couple of singletons which are bound in the singleton scope:

bind(TYPES.ICommandStack).to(CommandStack).inSingletonScope()

Provider Bindings

Sprotty's circular event flow introduces a cyclic dependency between the components ActionDispatcher, CommandStack and Viewer. To handle these, we have to use provider bindings.