Skip to content

Dependency Injection with Inversify

Graham Wheeler edited this page Sep 3, 2020 · 6 revisions

The VS Code extension for Python uses Inversify, an inversion of control (IoC) container for Typescript and Javascript applications. Inversify performs dependency injection, and is intended to help developers adhere to the dependency inversion principle. IoC containers are distinct from traditional IoC, and dependency injection and dependency inversion are not the same thing. All four concepts are related, distinct, and relevant to how our codebase is written.

Definitions

The dependency inversion principle (DIP) refers to depending on abstractions (i.e. interfaces) instead of concretions (i.e. classes). This is a design pattern that is part of the SOLID principles, and is not specific to Inversify and Typescript. Under DIP, classes define the interfaces they need from their dependencies, and the dependencies implement those interfaces. Variables should be typed using these interfaces rather than concrete classes.

Dependency injection (DI) refers to the technique of passing dependencies that a class needs to the class. This is one form of the broader technique of inversion of control. Inversify helps with dependency injection, but it's not the only way that you can inject dependencies into a class. Any class whose dependencies are explicitly passed in could also be said to employ dependency injection. There are two primary styles of dependency injection:

  1. Constructor injection: injecting dependencies into the class' constructor signature
  2. Property injection: injecting dependencies into a class property after instantiation.
  • NB: Our codebase uses constructor injection.

An inversion of control container is a specific style of inversion of control. Generally, inversion of control refers to handing the program flow over to a framework. An IoC container is a framework for implementing dependency injection. IoC containers also create and manage the life cycle of objects so we don't have to explicitly do that ourselves. Inversify provides such an IoC container.

Why do we use dependency injection and Inversify?

For the VS Code Python extension, dependency injection offers one major benefit: it allows us to mock dependencies of the classes we want to write unit and function tests for so we can test specific functionality. Without dependency injection, constructors would internally instantiate their dependencies and, in so doing, obscure those dependencies.

By using Inversify's IoC container for dependency injection, we can mock a dependency of a class we want to test based on its interface and rebind that object in the Inversify container to the mocked object instead. IoC containers also help us avoid verbose constructor calls when using dependency injection.

Since we've defined interfaces and bound them to concrete implementations, we get dependency inversion in the process as well--consumers of our classes depend on the interfaces and not the class implementation.

A general rule of thumb is that we only add classes to the container with an interface if:

  1. We need to test them separately
  2. We expect that their implementation might change in the future.

How we use Inversify for dependency injection

  1. For each class we define, it should implement an interface which specifies the public API for that class.
class InteractiveWindow implements IInteractiveWindow {
  • The interface IInteractiveWindow is defined in types.ts, which is where we put all our interface definitions.
  • We declare a symbol that is used as the service identifier for the interface:
export const IInteractiveWindow = Symbol('IInteractiveWindow');
  1. Next, we use Inversify's container.bind() method to associate the class constructor with the service identifier representing the interface, within the IoC container provided by Inversify.
  • NB: The VS Code Python extension codebase has multiple serviceRegistry.ts files. We have a custom-defined ServiceManager class which calls the Inversify container.bind() method under the hood.
  • We bind the InteractiveWindow class constructor to its service identifier, IInteractiveWindow, in the serviceRegistry.ts file:
serviceManager.add<IInteractiveWindow>(IInteractiveWindow, InteractiveWindow);
  • NB: An interface can only be bound to one implementing class at any point in an application.
  1. Each class we define is annotated with the Inversify decorator function @injectable().
  • The InteractiveWindow class is made injectable as follows:
@injectable()
export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> implements IInteractiveWindow {
  1. Within each class's constructor, instead of instantiating objects that the constructor depends on by calling their constructors directly, we inject them in the constructor signature, as follows. Each injected dependency is typed as an interface rather than the class itself:
constructor(
	@multiInject(IInteractiveWindowListener) private readonly listeners: IInteractiveWindowListener[],
	@inject(ILiveShareApi) private liveShare : ILiveShareApi,
	@inject(IApplicationShell) private applicationShell: IApplicationShell,
	)
  • NB: A consequence of using IoC containers is that we almost never directly call class constructors. Instead, we "request" objects from the IoC container, and the constructor is called by the container if it's the first time that we're trying to retrieve that object. In our case, we have a custom-defined ServiceContainer class which calls the Inversify container's .get() method under the hood. The container.get() call will call the constructor for us if it hasn't already been instantiated. If it has, the container.get() call just retrieves and returns that object from the container.
Clone this wiki locally