Skip to main content

Dependency Injection in JavaScript

Implementing Constructor Injection with the help of JavaScript Proxies. Remove hardcoded dependencies in Code and provide interchangeable strategies for algorithms by injecting dependencies using an Inversion of Control-Container.

info
This article was originally published in December 2022 at medium.com. Some formatting might have get lost during the migration to this site: If you think you spotted an issue caused by malformed formatting, feel free to open a Pull Request or send me an Email.

Motivation

Low coupling and high cohesion does not imply the total absence of associations: The usage of abstraction layers rightfully demands specifics that provide concrete implementations satisfying contracts between parts of a system. After all, a program doesn’t work if there is nothing that conforms to its Interfaces and Abstract Classes.

{title}
Figure 1 The most basic expectation regarding a computer program is that a given input produces some output.

However, the way these dependencies are wired throughout source code often comes with a bitter taste: Dependencies are found in convoluted and deeply nested program code, effectively violating SRP and DIP.

We are in the need of tools and design patterns that can help us with detangling the code, or we will end up with a program that has intricate boilerplate code for setting up mocks and stubs for testing, or worse: Becomes a nightmare to integrate with different environments and infrastructures. Granted, languages like JavaScript makes it easy to mock dependencies during tests, but other languages are not so forgiving and test cases tend to get more complicated the more dependencies are hardwired.

The following source code was taken from a REPOSITORY-implementation that uses a concrete Storage-class that hides away infrastructure that is used for writing data:

    class DataRepository {

async storeData(data) {
const {Storage} = await import("storageApi");

const store = new Storage(...);

store.save(data);
}

}

It seems to align nicely with our code standards — it’s small, it explains its intends clearly without additional comments, and it delegates to a sub-program responsible for communicating with infrastructure that is not of interest to us.

However, this method uses a hardwired dependency to storageApi which gets imported as a module. Even more, everything the constructor of Storage requires has to be handled from inside the storeData()-method. For testing this code, the developer has to create a Mock not only for Storage, but also needs to stub the call to import. The DataRepository directly accesses the low-level API from within its boundaries, which results in strong coupling between two different layers. This should not happen in the given storeData()-method, although the DataRepository obviously has to know that there must be some kind of infrastructure-layer.

{title}
Figure 2 The “DataRepository” directly accessing “Storage” creates strong coupling between two concretes that should be unaware of each other.

The Dependency Inversion Principle requires high-level modules to not import anything from low-level modules. This is one of the SOLID design principles. [📖ASD]

{title}
Figure 3 The “DataRepository” is configured with the “Storage”-instance. Now, the repository can focus on using the Storage’s public API and doesn’t have to take care of importing and configuring it. In tests, the API of the “Storage”-instance can easily be mocked.

This article introduces an Inversion of Control (IoC)-Container that decides during runtime if code has additional dependencies defined, and if any existing dependency should be resolved by the IoC-Container. This is realised through bindings configured by a client and passed to the the IoC-Container: These provide information for the concrete that has to be instantiated for a type, i.e. an Interface, or any (Abstract) Class,* *required by an arbitrary host.

Bindings can be used and adjusted application-wide, but it’s good practice to provide them during bootstrapping. This makes it easy to run programs with different implementations for selected clients, contexts or environments, and this all works without having to change a single line of low/high-level code at all.

{title}
Figure 4 Our Proxy wraps the constructors of selected target classes and injects dependencies with arguments as needed.

Proxies help with the implementation of resolving objects and dependencies, and this approach is not exclusive to JavaScript: For example, Java has Proxies, Spring uses them for its IoC and AOP.

If you need to catch up with the concept of Proxies and how they work in JavaScript, I recommend you read through this elaborate article that provides details on how to use Proxies with *Promises* to create Fluent Interfaces.

For the following examples, I will constantly refer to the IoC-Container implementation of coon.core.ioc: This is an implementation specific for the Sencha Ext JS framework, but it’s concepts can easily be carried over to other frameworks or framework-agnostic code.

How it works

Target classes need to provide information if they are injectable, i.e. if they should be considered by the IoC-Container during instantiation. This is needed because we want to auto-wire dependencies to keep our program as flexible as possible: The IoC-Container gets configured with bindings during the startup sequence, then takes care of publishing the configured types with their concrete implementations during the runtime of the application.

{title}
Figure 5 The IoC-Container will take care of dynamically resolving dependencies for a concrete instance that is required by the client.

Most programming languages and their platforms already provide the tools for handling additional information written with source code: Metadata is often created with the help of annotations in Java, or Attributes in PHP8.

Metadata: Static builds vs. runtime configuration

With the almost incomprehensible amount of tooling options for JavaScript, using annotations would probably cost little effort; however, it would most certainly mean that the build stack of our project changes: An additional tool that has the implementation for parsing our source code also extracts and translates metadata and makes sure that the resulting build does not break during runtime.

An annotation in the form of

    /**
* @injectable store:Storage
*/
class Repository {
// ...
}

would be useful, and additional annotations can be defined in some sort of dictionary.

The parser could then build configuration files out of the metadata found in the sources, connect them with the names of the target classes (and the paths to the imports), along with the properties (i.e. names of the instance variables of the target classes) which expect a particular type, and then plug it all together by applying the bindings configured by the developers and stored in the IoC-Container.

{title}
Figure 6 Using annotations with JavaScript code would require a separate build step in the ci/cd pipeline.

We strive for an implementation that does not need such additional tooling: We will provide the metadata as **static class members on top of the injectable classes.

The following source code demonstrates the use of the static-property: A property named required is the root for the meta information looked up by the IoC-Container and its Dependency Resolver: It holds all the names of the instance variables that expect specifics of a given type: In the example, an instance of Repository only works with a store-member that holds a reference to an instance of Storage.

    class Repository {
static required = {
store: "Storage"
}

//...
}

With the help of the Proxy-Api, we can then add a trap for the calls to the constructor of the injectable classes, in this case the Repository-class:

    class Repository {
static required = {
store: "Storage"
};

constructor({store}) {
this.store = store;
}

//...
}


const constructorHandler = {

construct (target, argumentsList, newTarget) {
if (target.required) {
// container holds a reference to the ioc-container
container.inject(argumentsList, target.require);
}

return new target(...argumentsList);
}

};


Repository = new Proxy(Repository, constructorHandler)

The handler will delegate to the IoC-Container before the instance of the target class is created: The IoC-Container then inspects the argument-list and looks for any missing properties in a previously contracted argument-object that’s used to configure the instance. Denoted by the required-property, the instance variable’s name must be the same as the configuration object containing the property needed by the constructor:

    // IoC-container will not inject anything, since the instance gets configured
// with a "store"-property
new Repository({store: new Storage(), uri: "/resourceUri"});


// since the "store"-property is missing, the IoC-container will
// inject a concrete of "Storage" according to the available bindings
new Repository({uri: "/resourceUri"});

Creating the Bindings

Bindings are the Point of Truth for our application since we rely on builders and resolvers that are configured upfront and take care of assembling associations during runtime. Bindings map concrete Sub Types to Types, means: They “bind” a typed variable to the specific implementation of the Type, so our IoC-Container knows what to apply and where to apply it (the when is implied by the usage of a Constructor Injector). The requested specific implements an interface or extends an (abstract) class and the LSP gives us the freedom to provide arbitrary implementations of this given Type.

Since we are loosely typed, our Dependency Resolver (think of it as sort of a Builder, ) must and will make sure that our specifics are indeed instances of the required type.

Finding a common language

We will introduce a model language that will help us with formulating the bindings needed throughout our program.

We have a class A that uses an instance of a class B:

{title}
Figure 7 A needs B.

The code for

A has a dependency to Type B, and this dependency is reflected in A’s instance variable b

could look like this:

    // Pseudo code

abstract class B {
abstract calculate();
}


class A {

constructor (B b)
{
this.b = b;
}


calculation()
{
this.b.calculate();
}
}

Obviously, sources that rely on A will not work without an [instanceof A].b — as soon as calculation() delegates to b.calculate()and b is undefined, an exception will be thrown.

We are looking for a formal (yet simple) definition that can be used with JavaScript to configure these dependencies: We’ll agree on JSON as the format, since it allows for key/value-pairs whereas the keys are of type string and their values can be any of string, integer, boolean, NULL, object and array — we’ll make use of string and object.

Let’s refine the task for resolving the dependencies of A:

    **when** A
**requires** B
**give** *new instance* of B

That’s a rather simple term which will be translated later into an assignment by the Dependency Resolver. For now, this is how it’s transposed to JSON (don’t mind the explanatory comments):

     {
/* when */
"A": {
/* "needs": "give" */
"B" : "InstanceOfB"
}
}

Use Case: Injecting Authentication Methods

With coon.core.ioc as part of a coon.js-application, here’s a typical call to coon.core.ioc.Container.bind():

    // Some class names have been shortened in favor of
// readability.
coon.core.ioc.Container.bind({
"conjoon.dev.cn_mailsim": {
"conjoon.SimletAdapter": "conjoon.BasicAuthSimletAdapter"
},
"conjoon.cn_mail": {
"coon.core.data.request.Configurator": {
"$ref": "#/$defs/RequestConfiguratorSingleton"
}
},
"$defs": {
"RequestConfiguratorSingleton": {
"xclass": "conjoon.cn_imapuser.data.request.Configurator",
"singleton": true
}
}

});

This configuration represents bindings of the extjs-app-imapuser-package, an npm package providing user authentication for conjoon’s extjs-app-webmail, which is an email client written in JavaScript.

{title}
Figure 1 The Login-Screen for the JavaScript webmail client used in conjoon, depending on the authentication module configured with this instance.

extjs-app-webmail communicates with a backend that is agnostic of the authentication being used — its architecture allows for guarding the endpoints with arbitrary authentication methods: It could be Basic access authentication, or the API could use a guard that relies on token based authentication. That is why the requesting client — in this case extjs-app-webmail — needs to be configured with the proper security technique the backend understands. This is done by using Request Configurators that hook into (some/all/none at all) outgoing requests and add the authentication information if required by the backend, e.g. a Authorization-header field, holding Bearer- ,Basic- or other information.

Bindings explained

Let’s have a detailed look at the given binding configuration. First of, the bindings configured here are introduced with namespaces instead of class names. This is just another way of defining bindings for a set of classes owned by a namespace (i.e. a whole module): Instead of individually defining dependencies for

conjoon.dev.cn_mailsim.A, conjoon.dev.cn_mailsim.B, conjoon.dev.cn_mailsim.C, …

we fall back to their common namespace, so the Dependency Resolver can look up bindings configured for this module when a target class is not explicitly specified in the configuration. Target classes are always given precedence, then namespaces are queried.

The same goes for the following section, albeit the value of the give- implication is not the name of a class: It’s a configuration that references another section.

      "conjoon.cn_mail": {
"coon.core.data.request.Configurator": {
"$ref": "#/$defs/RequestConfiguratorSingleton"
}
}

Based on the JSON-schema specification, $ref uses an URI to reference another section of the document it’s embedded in, which allows for defining a reusable, complex configuration at one place, then re-use this configuration throughout the document by referencing it.

The (resolved)$ref in the above example states that

    when any class of conjoon.cn_mail
requires coon.core.data.request.Configurator
give Singleton of conjoon.cn_imapuser.data.request.Configurator

Singletons are great when stateless objects are needed, and reduce the memory footprint in a target application.

Resolving dependencies — trapping the Factories

The class-system of Sencha Ext JS makes — almost exclusively — use of Factories when instances are created. This is useful for dynamically loading classes: Its microloader will take care of mapping class-names to the existing directory-structure of a project, but loading happens synchronously (in the worst case, if a class was not pre-loaded). Using Sencha Ext JS without it’s own class system is almost impossible. In some cases, this is an unpleasant surprise for users starting with Ext JS in 2022.

{title}
Figure 8 Sencha Ext JS Factory methods take care of resolving dependencies and instantiating objects.

However, the use of factories and factory methods throughout the framework makes it really easy to apply Proxies to them and plays into our hands with our constructor injection approach. Carefully selecting constructors of injectable target classes becomes obsolete and we can rely on the interiors of the framework when we have to decide if dependencies need to be injected.

A Wrapper-Proxy is installed as soon as coon.core.ioc.Container.bind() is called:

     installProxies () {
const me = this;

Ext.Factory = new Proxy(
Ext.Factory,
Ext.create("coon.core.ioc.sencha.resolver.FactoryHandler")
);

Ext.create = new Proxy(
Ext.create,
Ext.create("coon.core.ioc.sencha.resolver.CreateHandler")
);
},

There are two Proxy-Handlers that serve two different purposes. Let’s have a look at them:

Handler for Ext.create

The CreateHandler is a method that traps calls to Ext.create. It checks if the argument passed to Ext.create() is

  • a String: If that is the case, it’s assumed to be the name of the class an instance should be created for

  • an Object: This generally indicates that the client submits a configuration, holding an xtype or xclass providing further details on the class that serves as the template. All the properties the object contains are usually passed to the constructor of the target class, except for xtype/ xclass

{
"xtype": "alias-of-class",
// or "xclass": "fqn.of.class"
"cArg1": "foo",
"cArg2": "bar"
}

As Figure 9 shows, once the target class was successfully resolved given the internals of Ext JS, the handler fires the classresolved-event, along with information about the class name and the JavaScript prototype of the resolved class.

{title}
Figure 9 The Handler installed for Ext.create fires an event as soon as a class was successfully resolved.

Handler for Ext.Factory

The FactoryHandler implements traps for properties requested by a client (using get) and a trap for any method that could possibly be a factory method.

Factory methods are based on types that get published with class definitions. Aliases are constantly used throughout Sencha Ext JS and facilitate lazy instantiation. Those aliases are using prefixes which represent the domain they serve, for example, aliases for Ext.data.Store have the prefix "store.”, Ext.app.Controller use the prefix "controller.” and so on.

The Ext.Factory wires these configurations through to the corresponding factory methods, and that’s where the apply-handler previously installed by the get-handler comes into play: It works just like the CreateHandler and differs only in subtle details when arguments to the factory methods are scrutinized. In the end, this proxy will trigger the classresolved-event for the same reason as its complement.

    if (cls) {
const className = Ext.ClassManager.getName(cls);
me.fireEvent("classresolved", me, className, cls);
}

Proxying the constructors

It all boils down to the ConstructorInjector: Once the classresolved-event is published, the ConstructorInjector is used with an observer to decide whether it should inject dependencies into the target class’ constructor (the information about the target class is exposed with the event details). It checks whether the class is injectable and if that’s the case, it will apply a trap for the target class’ constructor.

Think of the Strategy Pattern here that allows us to dynamically change implementation details. It should be noted that the ConstructorInjector is working on objects passed to the constructor as arguments, rather than a list of arguments. So the ConstructorInjector is more of a Property Injector. The name was chosen since ConstructorInjector better reflects the step in the build chain the injector is woven into: The implementation for the Sencha Ext JS framework is kept simple and provides mainly (but not less than) qol-improvements for this framework.

The trap for the constructor will probe the target class for all the dependencies required (defined as metainformation), then use the Dependency Resolver to create something useful out of the binding definition that was previously registered with coon.core.io.Container.bind().

{title}
Figure 10 When the client requests a new instance, the ConstructorInjector will make sure that dependencies required by the target instance are created and injected, if not already specified by the client.

Dependency Injection in Angular

In Angular, Dependency Injection is already built into the framework. Conversely to the approach above where we set up the injectable classes in a global configuration file, Angular lets you decorate a class as Injectable by using the @Injectable()-Decorator:

Image.service.ts [Angular]
@Injectable({
providedIn: 'root'
})
class ImageService {}

If we had such a class that provides an abstraction to an ImageService, registering the service as an injectable Singleton in an extjsapp namespaced Ext JS-application would translate to the following:

{
"ioc": {
"bindings": {
"extjsapp": {
"ImageService": {
"xclass": "ImageService",
"singleton": true
}
}
}
}
}

Constructor-binding works out-of the box with Angular. To register the ImageService with a host, simply add the ImageService as an argument for the host's constructor:

Host.component.ts [Angular]
import {ImageService} from "./Image.service";

class HostComponent {
constructor (private imageService: ImageService) {}
}

whereas we'd have to denote the requirement in the statics.required-property to realize constructor injection:

HostComponent.js [Ext JS]
Ext.define("HostComponent", {
statics: {
required: {
imageService: "ImageService"
}
},
// ....
})

Additional Resources

The repository for coon.js and the IoC-Container implementation is located at Github. It’s already used in the newest version of conjoon 1.0.4 which is an interim release that paves the way for additional authentication plugins planned for 1.1.0.


Significant Revisions

  • 30 March 2023: Add section "Dependency Injection in Angular"

  • 05 March 2023: Merged article into this site

  • 16 January 2023: Minor wording and content updates while working on the german translation

  • 17 December 2022: Published first installment