In this article we will describe the basic concepts of dependency injection with inversify, which are required to implement extensions for Eclipse Theia. The main two use cases are (1) to use a service or a class provided by the platform and (2) to contribute an extension to the platform. In both cases, inversify is the glue between your extension and Theia.
Eclipse Theia is a platform to develop web-based tools and IDEs. More details about this new Eclipse project can be found in this article. One of the core benefits of Eclipse Theia is its extensibility. This extensibility is achieved by relying heavily on dependency injection based on “inversify”. If you are familiar with the Eclipse Platform, inversify replaces extension points and OSGi declarative services. If you are familiar with Google Guice, inversify is pretty much the same with only small differences. When getting started with Eclipse Theia, inversify is probably the most important new thing to learn (besides TypeScript, HTML, and CSS). Therefore, we describe the most important concepts of inversify used in Theia in this article along with some examples.
In this section, we introduce the basic concept of decoupling a contribution provider from a contribution consumer. If you familiar with any other DI framework or a similar concept such as OSGi DS or extension points, you might want to skip this section and continue with the concrete examples in the following two sections.
The basic pattern behind all of this is a decoupling between a contribution that some consumer wants to call and the retrieval of one or more concrete implementations of this contribution. The “contribution” is usually described as an interface, sometimes also directly as a class. As an example of a contribution, a consumer might want to call one or more implementations of the interface “Printer” to “print” something. Using dependency injection, you can get implementations of “Printer”, e.g. “PrinterImpl” without having a dependency on PrinterImpl. Instead, you just need to know (depend on) the interface “Printer”. In turn, an arbitrary number of providers can provide implementations of “Printer”.
The following diagram shows this pattern with more abstract terms: “Interface” (i.e. Printer), “Contribution” (i.e. PrinterImpl) and “ContributionConsumer” (i.e. the class which calls “print”).
The decoupling between the interface and a concrete implementation is achieved through a central registry. When using dependency injection this registry is called a dependency injection context. This is simply speaking a map using interfaces as a key and implementations of those interfaces as a value. Providers can push implementations to this map (using the interface as a key) while consumers can retrieve implementations from this map, again using the interface as a key. As a consequence, the consumer can retrieve the implementation, without depending on it.
Please note that a contribution provider, which also defines an interface for the contributions that it expects, is also sometimes referred to as a “Contribution Point Provider” because it provides a “Point” where a contribution can be provided by another component.
Now let’s relate this basic pattern to the use cases in a platform such as Eclipse Theia, which uses this decoupling in both directions to achieve extensibility: In order to consume contributions and to provide them – without imposing a dependency between the contribution provider and the contribution consumer. The following diagram shows an overview of all relevant use cases. First, contributions can be consumed by an external extension or by the platform itself (External and Internal Contribution Consumer). Please note that a consumer can either use one implementation, or a list of them.
Second, contributions can be provided for a certain interface, again, either by an external provider or by the platform itself (Internal and External Contribution Provider). Please note that it technically does not matter whether a contribution is provided or consumed from within or from outside of the platform. In fact, Eclipse Theia uses these concepts internally as well, so basically the platform itself consists of extensions. So at the end, everything is about contribution provider and consumer, no matter whether they live within the platform or in an extension.
In the next two sections, we show how to consume and provide contributions in Eclipse Theia along concrete code examples using inversify.
Consuming Contributions with inversify
In this use case, you want to consume/retrieve a certain component (i.e. implementation of an interface). This can be a central service provided by the platform (e.g. the FileSystem), a contribution by another extension, or even something you contributed yourself in the same extension. Essentially, there are two places in a class, where you can retrieve a contribution, as a parameter in the constructor and directly as fields within the class. In the example “SimpleConsumer” below, InterfaceA is retrieve as a field while InterfaceB is retrieved as a parameter in the constructor. In both cases, you specify the field/parameter as usual, but add the annotation @inject with the interface as a parameter. This tells inversify which interface to look up in the context, so that it will automatically fill in the right parameter. Finally, all classes, which should be instantiated via dependency injection must be annotated with @injectable().
While the usage of Symbols is preferred, you can technically retrieve the contributions for an interface by using the name as a String, too. This only works, if the same String is used during the registration. Therefore, it makes sense to extract the String as a constant or Symbol.
All examples so far are only expecting one implementation to be retrieved. However, there are cases, where you expect an arbitrary number of contributions and therefore want to retrieve a set of implementations. This can be achieved with the annotation @multiinject, as shown below. In this case, you also need to adapt the consuming code to handle an array of retrieved contributions. Please note that because you do not depend on the contribution provider, you never know exactly how many contributions you will retrieve.
Finally, as shown in the method “doSth” in the example above, all injections can directly be called, since TypeScript allows you to assign constructor parameters as fields without explicitly defining and assigning them. As shown before, you could also directly inject them as fields. To keep a good class design, we still recommend injecting in the constructor rather than as fields. This way, a class remains usable without also using dependency injection. The constructor still defines the required parameters, e.g. for manual instantiation of a class in a test case:
Making Contributions with inversify
In the opposite use case, you want to register (provide) a contribution. The most common case is to register a contribution using an interface defined by the platform to extend Theia (e.g. to add a menu item). You can also register contributions, which are defined by you and consume them elsewhere.
Again there are a few cases, which are commonly used in Eclipse Theia. The simplest case is to register a class under a certain interface. First, you create the class as you would without inversify and mark it with the annotation @injectable(). This tells the framework, that this class can be instantiated via dependency injection:
Registering this class under a certain interface is typically done in a module as shown below. There is typically a module per extension for the frontend and a separate module for the backend doing all bindings. So this module is a bit like the plugin.xml used to be for extension points. The example below just binds ClassA to InterfaceA. By calling the bind function again in the same module, you can add an additional binding, e.g. to bind another class against the same interface or any other binding.
To make Theia aware of your modules, you need to register them in the package.json:
Now if any other component retrieves instances of InterfaceA, inversify will instantiate ClassA, put it into the context and use it as a parameter for the consumer.
Please note that you can also register a class under more than one interface if it implements them, as shown below:
… and the binding in the module:
As mentioned before, sometimes, there is no interface defined. In those cases, you can also just register a class without an interface:
Which is then bound “to itself”:
As an alternative to binding to a class, you can also bind to a “dynamic value”, i.e. a function, which returns the expected type. For instance, this can be used when the value to be bound is a proxy. The following example binds to a function which is implemented inline:
Finally, inversify also supports the so called “autoBindInjectable mode”. In this mode, classes without an interface, like the MyClass above, do not explicitly have to be bound to be available via dependency injection. However, this mode is turned off by default in Theia, so every binding must be explicitly specified.
Example Contributions in Eclipse Theia
Let’s look at two examples (one for each use case) of how those concepts are used in Theia. First, let us look at a typical service you consume from the platform, the MessageService. In the following code examples, we retrieve the service in the constructor of a class and call it in a function below. Please note that this will only work in a class, which is created via dependency injection, which is typically a contribution itself.
In the second example, we register a contribution to be picked up by the platform, a MenuContribution. First, we create a class implementing the corresponding interface. In this example, the function “registerMenus” will be called by the platform.
To register our contribution, we need to bind it to the corresponding interface (a Symbol representing it). This is typically done in a “*Module” class within an extension:
Testing and manual Injection
If you want to test the injection independently of Theia and check that all annotations are set correctly, you can also create your own custom injection container and manually fill it with bindings by adding your module. On this container, you can use a function to retrieve all contributions that have been registered in your module. The following example instantiates a container, loads our custom module, and retrieves a consumer. This consumer is filled with all the contributions in the module. Please note that we can only directly retrieve the consumer class, because we turned the “autoBindInjectable mode” on.
Please note that the code above is an integration test combining your module and the consumer. If you wish to isolate your test from other contributions bound in a module, you can also manually create a DI container which only contains the contributions you need for the test case, e.g. like this:
Finally, as mentioned at the end of section “Consuming Contributions with inversify”, you can also just fill in parameters without using dependency injection. In this case, you can simply pass a mock class without having to create a dependency injection container at all (as shown below).
Inversify is a light-weight and easy-to-use dependency injection framework. It allows decoupling the provider and the consumer of a contribution and therefore enables extensibility. The design also improves testability, as components can be instantiated with mock objects for the parameters they require at runtime. To sum up, inversify provides a good replacement for the concepts “extension points” and “OSGi declarative services” as used in the Eclipse Platform because the underlying concept is very similar.
Eclipse Theia is extensively using inversify. It is worth mentioning that the platform itself uses the same mechanism. There is conceptually no difference between a custom extension and a platform module. This is a great design, as all components “play by the same rules” and you can extend or replace many parts of Theia, even if the developers have not explicitly foreseen your use case. If you are missing any additional details on this topic or have a question about your custom use case, feel free to get in contact with us!