-
Notifications
You must be signed in to change notification settings - Fork 83
Dependency Injection
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.
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.
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.
InversifyJS requires us to mark classes as @injectable
.
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()
...
}
We use a couple of singletons which are bound in the singleton scope:
bind(TYPES.ICommandStack).to(CommandStack).inSingletonScope()
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.