Dependency Injection
If you've followed the documentation so far, you've already seen ModuleManager dependency injection (DI) in action.
Just to take a step back, the idea is that instead of a class having a direct reference to a dependency, you instead inject the dependency from outside. This yields great benefits architecturally, as it implements the Dependency Inversion Principle (the "D" in SOLID), which enables the Liskov Substitution Principle (the "L"). The practical result is loose coupling, where you can switch between different concrete implementations as long as they match an abstract interface.
At its core, dependency injection can be very simple, and generally takes one of three forms:
- pass the dependency into the constructor
- pass the dependency into a method
- set the dependency directly onto a property (or setter)
Ideally, your class would only depend on an interface rather than a concrete implementation. Unfortunately, interfaces don't actually exist in JavaScript (you can fake them in TypeScript, but as they don't exist in the runtime, they lose a lot of their power).
For this reason, ModuleManager doesn't rely on language-enforced types; it uses a much simpler
system based on names, with some "opt-in" extra runtime validation.
Building on the Schema library, module types are simply a schema with a transformation pipeline that converts
strings into instances.
Interfaces or abstract base classes are represented by the provides module setting, as a synthetic "type" that
needs to be specified by one of a matching set of implementations. There's no language-level type checking, the schema
created by ModuleManager simply checks whether a named module assignment matches one of the known named providers.
Initialization
ModuleManager deliberately avoids approach (1), constructor-based DI1. The library instead wants to be able
to instantiate modules with a no-argument constructor. Separating construction from initialization greatly
simplifies handling circularity in the module dependency graph.
You have the choice to either use approach (2) by implementing an init lifecycle method that accepts
dependencies passed an argument, or to use approach (3) via the inject module option (set by default
if no init method exists) to have dependencies directly set on the module instance.
Contrast hello-world-advanced
with hello-world-complicated
for examples that demonstrate both approaches.
In both approaches, initialization is managed as a phased lifecycle event. During this phase, applications should
limit their access to other modules, and instead just store a reference that can be used after the start lifecycle
has been initiated.
In the next section these phases are discussed in more detail.