icon-search
Dependency Injection Angular

Dependency Injection in Angular 2

Paweł Kozioł 18.10.2016

Dependency Injection is a well known design pattern and is one of the noteworthy features of the Angular framework. It helps engineers reuse, maintain, and test the code. It has become so popular, that it is hard to imagine a programming framework without this feature. Dependency Injection is closely related to the Inversion of Control pattern and Dependency Inversion, which stands for “D” in the SOLID object-oriented design acronym.

Dependency Injection basics

You might have seen the car-engine-tyre example in the angular documentation blog. Following the same approach, DI is about providing required objects (Engine and tyres) to a component (car). Dependencies are generally injected in a constructor.
So, without DI you’ll have:

constructor() {
  this.engine = new Engine();
  this.tires = new Tires();
}

And with DI:

constructor(public engine: Engine, public tires: Tires) { }

As you can see, the difference lies in the component creating or receiving the dependencies. DI can change spaghetti code to a maintainable code in a big system.

More advanced DI

DocumentPreviewComponent example

Let’s take a look at a more complex example from one of Sparkbit Angular applications. The DocumentPreviewComponent displays documents to authorised users. It uses a REST API to communicate with the application backend over HTTP. It’s a simple component that depends on ten other classes to complete its job. These classes also depend on each other, which creates a complex dependency graph.

Imagine all the boilerplate code we would need to create instances of all dependencies!

Dependency Injection example - DocumentPreviewComponent's dependencies

Dependencies

  • DocumentService – provides documents and their metadata to the component,
  • UserService – manages users and their permissions. Helps determine whether a user has rights to a particular document,
  • UIConfigurationService – manages UI configuration for users. Decides who sees which parts of the application,
  • JsonParser – utility to parse JSON responses from the REST API,
  • ProgressBarService – controls and displays the progress of HTTP requests,
  • NotificationService – manages notifications sent to components after completion of certain actions. For example, when a user marks a document as “read”, a notification is sent to the documents list and its contents are updated,
  • Http – angular HTTP component. It will later be replaced by our custom decorated implementation,
  • Logger – our logging utility,
  • AnalyticsService – monitors application performance,
  • I18NService – UI internationalization service.

As you can see, a simple component may depend on many classes. These classes may also have their own dependencies. This dependency tree is difficult to maintain. DI makes things easier by freeing us from complex dependencies. DI shows most of its strength while testing because during testing, real dependencies are replaced by mock implementations.
It is especially useful for dependencies related to network connectivity or database access such as HTTP, UserService, or AppConfigurationService. We’ll go into more detail about tests later. Let’s focus on useful Angular DI constructs for now.

How it is done in Angular

Constructor

First let’s look at the DocumentPreviewComponent’s constructor. Just like with the Car’s constructor, it takes all dependencies as parameters.

constructor(private documentService: DocumentService, private userService: UserService, 
    private appConfigurationService: UIConfigurationService, jsonParser: JsonParser,
    progressBarService: ProgressBarService, notificationService: NotificationService, http: Http,
    logger: Logger, analyticsService: AnalyticsService, i18nService: I18NService)

There is nothing special about the DocumentPreviewComponent. We do not have to annotate it in any special way. All services used by it are annotated with @Injectable() as in this example:

@Injectable()
export class NotificationService

NgModule

All the Dependency Injection “magic” happens in the main application module.

@NgModule({
    imports: [CommonModule, BrowserModule, HttpModule, LibraryModule],
    providers: [DocumentService, UserService, AppConfigurationService, NotificationService, 
        AnalyticsService, AppSettingsService, Logger, I18NService,
        {provide: BackendAppender, useFactory: function(http, log){
            return new BackendAppender(http, log, ENDPOINTS.logEndpoint());
        },
            deps: [Http, Logger]
        },
        {provide: ErrorHandler, useClass: LoggingExceptionHandler},
        JsonParser,
        {provide: JSONTransformations, useValue: typeJSONTransformations},
        RandomIdGenerator,
        ProgressBarService,
        {provide: APP_BASE_HREF, useValue : "/" }
    ],
    declarations: [MainAppComponent,
        DocumentListComponent, DocumentPreviewComponent, //...
    ],
    bootstrap: [MainAppComponent]
})
export class MainAppModule {};

Let’s analyse, what happens here.

The imports field,specifies all external modules used by MainAppModule. These include CommonModule, BrowserModule, and HttpModule from Angular and our own LibraryModule. In addition to that, there is the providers section, where we specify all the services that will become available in the application’s root dependency injector and thus everywhere else in the application. Finally, we have the declarations, listing all the components and the bootstrap, specifying the main component.

The providers section defines the Dependency Injection configuration. Here we can find all the DI constructs offered by Angular. All of them are objects with the provide field.

Factory function

We have an object with the useFactory field. It informs Angular that the BackendAppender will be provided by a factory function. In our example we gave the function definition in place, but it can also be a reference to a function defined elsewhere.

This class has its own dependencies, which are specified by the deps field. Summing up, for useFactory we specify an object with provide, useFactory and deps fields.

Class provider

Next comes the {provide: ErrorHandler, useClass: LoggingExceptionHandler}. Here the useClass field is used to indicate injection of the LoggingExceptionHandler class instance in place of the standard Angular ErrorHandler class. This LoggingExceptionHandler class will be instantiated only once in this root injector. All its users will refer to the same injected singleton instance.
Comparing this to Google Guice for Java, it would be a Linked binding (bind(ErrorHandler.class).to(LoggingExceptionHandler.class)).

Value provider

Another construct is {provide: JSONTransformations, useValue: typeJSONTransformations}. It uses a specific value which is injected for the JSONTransformations type. In our application this value is defined in one of the libraries and looks like this.

export const typeJSONTransformations: ByTypeJSONTransformations = new ByTypeJSONTransformations({
     "string": [readStringField, readNumberFieldAsString],
     "number": [readNumberField, readStringAsNumber],
     "boolean": [readBooleanField, readStringAsBoolean],
     "date": [readStringField, readEpochTimestampAsDate],
     "array": [readArrayField, readSingleElementArrayField, readObjectAsArrayField]
});

We will go into more detail about these JSON transformations in one of the next blog posts. This is just an illustration of an injected object instance created outside the injector. In Google Guice it would be an Instance binding (bind(JSONTransformations.class).toInstance(typeJSONTransformations)).

Injecting non-class values

And finally we have {provide: APP_BASE_HREF, useValue : "/" }
APP_BASE_HREF is from angular/common and is of OpaqueToken type (export declare const APP_BASE_HREF: OpaqueToken;).
OpaqueToken is an Angular class that handles the problem of binding injected values to language constructs other than classes. We cannot use interfaces for this, because they exist only in Typescript and are removed from the code that is transpiled to Javascript. Therefore, when we want to inject values like named string configuration constants, we use instances of the OpaqueToken class. Similarly in Guice we could write bind(String.class).annotatedWith(Names.named("APP_BASE_HREF")).toInstance("/");.
More information on the OpaqueToken can be found in Angular Dependency Injection documentation.

Other features

Angular also offers hierarchical injectors, so it is possible to have more than one injector tree in the application. In fact, there can be a separate injector for every component in the application. Consequently, it is possible to define different providers for each injector. While it looks like a cool feature, we didn’t find any use for it in our application.

DI for testing

Dependency Injection is the most useful when there is a possibility of reusing a class in a different context with changed dependencies. DI is mostly used in tests when original dependencies are replaced with mocks.

This is a unit test for a service delivering documents do the DocumentPreviewComponent.

describe("DocumentService", () => {

    class MockMultipartHelperService {
        private resp: SimpleResponse;
        public post(formData: FormData): Observable<SimpleResponse> {
            return Observable.of(this.resp);
        }
        public put(formData: FormData): Observable<SimpleResponse> {
            return Observable.of(this.resp);
        }
        public setResponse(resp: SimpleResponse) {
            this.resp = resp;
        }
    }

    function respondWithJSON(json: {}): Response {
        return new Response(new ResponseOptions({body: JSON.stringify(json)}));
    }

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [HttpModule],
            providers: [ProgressBarServiceStub, DocumentService, NotificationService,
                JsonParser, Logger, TpI18NService,
                MockBackend, MockMultipartHelperService,
                {provide: MultipartHelperService, 
                          useExisting: MockMultipartHelperService},
                {provide: ProgressBarService, 
                          useClass: ProgressBarServiceStub},
                {provide: XHRBackend, 
                          useExisting: MockBackend},
                {provide: ByTypeJSONTransformations, 
                          useValue: byTypeJSONTransformations}]
        });
    });

    it("should get JSON with document's metadata", async(inject([MockBackend, DocumentService], 
        (backend, DocumentService) => {
        const documentFieldsObservable = DocumentService.getDocumentFields();
        let response = {"documentFields": ["value1", "value2", "value3"]};

        const promise = documentFieldsObservable.do(fields => {
            expect(fields.length).toEqual(3);
            expect(fields[1]).toEqual("value2");
        }).toPromise(Promise);

        backend.connections.subscribe((connection) => connection.mockRespond(respondWithJSON(response)));
        return promise;
    })));

});

First, we implement a mock MultipartHelperService class and utility respondWithJSON method. It is followed by the Dependency Injection specification. The providers object is the same as in the previously mentioned NgModule. It includes the useExisting, useClass and useValue provider configurations.
Note that the backend is mocked, so whenever a service runs an HTTP request, it receives a mocked response. In addition, we use the respondWithJSON method to easily return JSON objects defined in the test. A MockMultipartHelperService is also used for multipart file upload.
In the end, we have the ProgressBarServiceStub, which simulates the behaviour of a real progress bar.

Final words

Summing up, Angular offers a Dependency Injection framework with functionalities similar to those offered by Spring or Guice for Java. It frees developers from the need to maintain a complex dependency tree. Moreover, it facilitates code reuse by letting us change the classes we depend on. This is especially useful in testing, where we need to use mocks instead of real implementations.

comments: 0