Skip to main content

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:

  1. pass the dependency into the constructor
  2. pass the dependency into a method
  3. 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.

Footnotes

  1. Constructors are a fine way to handle DI if you have no cycles in your dependency graph, and can lean on typechecking or language reflection for enforcement. In JavaScript, this can be very fragile. (If you've ever used DI in AngularJS (particularly 1.x), you have likely experienced the pain of trying to ensure that the constructor arguments are sequenced correctly!)