Dependency Injection in N4JS

Dependency Injection in N4JS

Dependency injection (DI) is a concept that allows for configuring dependencies between classes at a central location. Instead of passing dependencies from class to class, N4JS' built-in DI support does this automatically.

This makes the user code much cleaner, easier to maintain and also improves its testability. N4JS DI framework follows Java JSR-330/Google Guice, making its usage easy for Java developers.

Application Object Graph

In object oriented languages, applications are composed from objects that interact with each other. Instances of those objects need to be created and wired together on application startup to create a so-called object graph of the application. While it’s possible to manually create this object graph, it quickly becomes complicated. This is especially so if we want flexibility and reconfigurability to be long-lasting features of our application.

Solutions for manually wiring the object graph come with their distinct disadvantages:

  • Hard coding dependencies makes code inflexible and complicate testing.

  • Passing dependencies to constructors bloats the constructors.

  • Using factories requires passing the factory and also bloats the code.

Dependency Injection (DI)

Dependency injection (DI) and DI frameworks aim to help with issues described before, specifically with the following:

  • The object graph is created automatically which removes burden of writing object factories.

  • Injection of the created instances is done behind the scenes where needed, which separates object-creation from object-usage and keeps constructors simple.

  • The application’s configuration can be changed without changing its components.

N4JS provides built-in support for dependency injection using a lightweight syntax with annotation similar to Java JSR-330 / Google Guice. The N4JS testing framework also supports dependency injection which allows for special test settings in order to test components individually.

Example

In the following example, two versions of a simple weather application are implemented. Both versions use a module WeatherEngine which returns the temperature for a given city. For this example, we use a timeout to emulate a real request to a weather server:

export public class WeatherEngine {
    data = [ {city: 'Berlin', temp: 5}, {city: 'Hamburg', temp: 15}, {city: 'Palo Alto', temp: 10} ];

    public temperature(city: string): Promise<number, ?> {
        return new Promise<number, any>(
            (cb: {function(number)}) => {
                setTimeout(() => cb(this.data.find(e => e.city == city).temp) , Math.random() * 2000);
            });
    }
}

In order to keep the examples as small as possible, in the non-DI version no manual wiring of the dependencies is used. The components are instead set up by initializing the fields directly.

Without Dependency-Injection

With Dependency-Injection

WeatherApp.n4js
import { WeatherEngine } from 'WeatherEngine';

export class WeatherApp {
    private engine: WeatherEngine = new WeatherEngine();

    async printTemp(city: string): string {
        return city + ': ' + (await this.engine.temperature(city));
    }
}


export class Server {
    weatherApp: WeatherApp = new WeatherApp();

    async run() {
        for (var s of ['Berlin', 'Hamburg', 'Palo Alto']) {
            console.log(await this.weatherApp.printTemp(s));
        }
    }
}
WeatherAppDI.n4js
import { WeatherEngine } from 'WeatherEngine';

export class WeatherApp {
    @Inject private engine: WeatherEngine;

    async printTemp(city: string): string {
        return city + ': ' + (await this.engine.temperature(city));
    }
}

@GenerateInjector
export class Server {
    @Inject weatherApp: WeatherApp;

    async run() {
        for (var s of ['Berlin', 'Hamburg', 'Palo Alto']) {
            console.log(await this.weatherApp.printTemp(s));
        }
    }
}
Starter.n4js
import { Server } from 'WeatherApp'


var server = new Server();
server.run();
StarterDI.n4js
import { Server } from 'WeatherAppDI';
import { N4Injector } from 'n4js/lang/N4Injector';

var server = N4Injector.of(Server).create(Server);
server.run();

The changes are only minimal: Instead of creating the field instances directly, they are annotated with @Inject. This should be familiar to Java programmers having used Google Guice.

An interesting aspect of dependency injection is how to set up the injector. In N4JS, the annotation @GenerateInjector is used in order mark a class as a dependency injection component. In other words, to associate an injector with the class. Running the server now requires slightly different instantiation. Instead of constructing the server with new, the built-in class N4Injector is used to create the first instance.

Application Reconfigurability

A very useful quality of DI is its flexibility. This is particularly beneficial during testing. Let’s write a test class for our WeatherApp. We do not want to wait an arbitrary amount of seconds to receive the results of our test, we want to use a special version of the WeatherEngine which immediately returns a value. Let’s have a look at the test module:

import { WeatherApp } from 'WeatherAppDI';
import { WeatherEngine } from 'WeatherEngine';
import { Assert } from 'n4/mangel/assert/Assert';

class WeatherEngineMock extends WeatherEngine { (1)
    @Override
    public async temperature(city: string): number {
        return 1;
    }
}

@Binder (2)
@Bind(WeatherEngine, WeatherEngineMock)
class WeatherAppTestConfig{ }

@GenerateInjector() (3)
@UseBinder(WeatherAppTestConfig) (4)
export class WeatherAppTest {
    @Inject weatherApp: WeatherApp;

    @Test public async test() {
        Assert.equal(await this.weatherApp.printTemp('Berlin'), 'Berlin: 1');
    }
}
1 A mock engine must be written.
2 Followed by a "binder" which is a configuration telling the injector what type has to be used to instantiate objects. By default, the injector uses the same class as referenced in the code. We change this and bind the mock engine to the real engine.
3 As the N4JS test framework already supports DI, we can declare the test as a new dependency injection. component.
4 Specific test configuration. The important point is that the class WeatherApp now gets the WeatherEngineMock injected.

Advanced features

Specific advantages and extended DI features are discussed in greater detail in the N4JS language spec. Some of the most notable features are:

  • Built-in pseudo-scope via @Singleton.

  • Possibility of nesting injectors via @WithParentInjector.

  • Built-in Provider type and possibility to create custom providers via @Provides to dynamically create instances.

  • Automatic resolution of dependency cycles.

Quick Links