What DI provides:
But aside from providing services for fetching data, the Angular’s Dependency Injection Mechanism allows us to achieve some higher level of decoupling in our apps, and in this article I’m going to explore how.
If you are new to Angular or unfamiliar with the Dependency Injection, I suggest you first read what the official documentation has to say on the topic.
You may also like:Angular Dependency Injection Provider Objects
If you have been using Angular for a while, chances are you are familiar with environment.ts
files. These files are used to provide information about the environment in which our app is running. For example, if our app is running in a development environment, we want our data fetching services to point at the server app running on [http://localhost:3000/api](http://localhost:3000/api,)
, for example, and if it is running on the temporary server for the manual QA, we could want it to point to [https://qa.local.com/api](https://qa.local.com/api,)
, for instance. This is managed by Angular during build process with the usage of different environment files, for example, we could have two files called environment.ts
and environment.qa.ts
in our environments
folder, and when we run the command ng build --config qa
, the Angular CLI will replace our environment.ts
file with environment.qa.ts
and the app will run in the QA
mode respectively.
But what does DI have to do with this?
Take a look at this component:
import { Component, OnInit } from '@angular/core';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-some',
templateUrl: './some.component.html',
styleUrls: ['./some.component.css']
})
export class SomeComponent implements OnInit {
constructor() { }
ngOnInit() {
if (!environment.production) {
console.log('In development environment') {}
}
}
}
A naive usage of the environment variables
Here we just import the environment.ts
file and use its contents right away.
But what if we want to inject some other values inside this environment variables for unit testing? Also, have a look at this service that also makes use of the environment variables:
import { environment } from 'src/environments/environment';
@Injectable()
export class SomeService {
constructor(
private http: HttpClient,
) { }
getData(): Observable<any> {
return this.http.get(environment.baseUrl + '/api/method');
}
}
Using environment variable to point at an API
Again, this looks very normal, but in reality this is not the best way to reference an environment variable. Imagine that this service is a part of a multiple project Angular application. So we have a projects
folder, which contains different apps (web components, for instance, is a good example of such a complex app). Can we reuse this service in another project? Theoretically we can — just import it into another Angular module using the providers
array, right? But here comes the problem — different projects can have different environments! We cannot just import one of them, but we still have to ensure that this service can be reused by as many components as possible. We can achieve this by using DI.
There are several possible ways of achieving this functionality, but let’s start from the most simple one — Injection Tokens
.
Injection tokens are an Angular concept which allow us to declare independent unique dependency injection tokens to inject values into other classes using the
Inject
decorator. Read more about them here.
All we have to do is just provide the value for our environment in a module:
export const ENV = new InjectionToken('ENV');
@NgModule({
declarations: [
AppComponent,
SomeComponent
],
imports: [
BrowserModule
],
providers: [
{provide: ENV, useValue: environment}
],
bootstrap: [
AppComponent
]
})
export class AppModule { }
As you see, we have provided the value for our environment variable under an InjectionToken
, so now we can use it in our service:
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ENV } from '../app.module';
@Injectable()
export class SomeService {
constructor(
private http: HttpClient,
@Inject(ENV) private environment,
) { }
getData(): Observable<any> {
return this.http.get(this.environment.baseUrl + '');
}
}
Notice how we no longer import the environment.ts
file. Instead we bring the ENV
token and allow the app itself decide what value will be passed on to the service, depending on the project in which it is being used.
But still, this is not the best solution. We did not provide a type for the environment
private field, which is kind of self defeating — we need typings to make our code less error prone, and Angular can leverage typings to bring better DI — for example, we actually can get rid of the Inject
decorator and the InjectionToken
as a whole! Here’s what we are going to do: write a shell class to describe the interface of our environment variable, and then use it to provide the actual value. Here is an example of such a class:
export class Environment {
production: boolean;
baseUrl: string;
// some other fields maybe?
}
In this class we described what our environment actually looks like. But we can also use this class as an Injection Token for injecting the environment variable! Just using useValue
as a provider:
@NgModule({
declarations: [
AppComponent,
SomeComponent
],
imports: [
BrowserModule
],
providers: [
{provide: Environment, useValue: environment}
],
bootstrap: [
AppComponent
]
})
export class AppModule { }
And in our service:
@Injectable()
export class SomeService {
constructor(
private http: HttpClient,
private environment: Environment,
) { }
getData(): Observable<any> {
return this.http.get(this.environment.baseUrl + '');
}
}
This solution not only provides us with a cleaner, more usual DI mechanism, but also with static typing.
But there is still a very small problem. Consider this:
@Injectable()
export class SomeService {
constructor(
private environment: Environment,
) { }
someMethod() {
this.environment.baseUrl = 'something else';
}
}
Here we change the value of one of our environment variables. And because Angular modules make sure that within itself, every component gets the same instance of a dependency, doing this:
export class SomeComponent implements OnInit {
constructor(
private someService: SomeService,
private environment: Environment,
) { }
ngOnInit() {
this.someService.someMethod();
console.log(this.environment.baseUrl);
}
}
Will yield this:
So we “accidentally” changed an environment variable. This is not a very big concern (developers don’t usually go around randomly reassigning environment variables), but we still want to reinforce that rule. It is pretty easy — just make the fields on our class readonly
:
export class Environment {
readonly production: boolean;
readonly baseUrl: string;
// some other fields maybe?
}
Now we have the full power of DI, but are also protected against accidents.
We now know how to provide the environment variables via DI, but what about switching between different services (while preserving the API) depending on the environment?
Imagine the following situation: we need some crash reports/usage statistics from our application when it is running. But the nature of the logs differs depending from the environment we are using: in the development environment we want to just log an error or a warning to the console; in the QA environment, we want to call an API which will collect our errors in an Excel file and send them to the management; and in production, we want a separate log file on the backend, so we call another API. So we come up with an idea of having a Logger
service, which will handle this functionality. Here is a naive implementation of this service:
@Injectable()
export class LoggerService {
constructor(
private environment: Environment,
private http: HttpClient,
) { }
logError(text: string): void {
switch (this.environment.name) {
case 'development': {
console.error(text);
break;
}
case 'qa': {
this.http.post(this.environment.baseUrl + '/api/reports', {text})
.subscribe(/* handle http errors here*/);
break;
}
case 'production': {
this.http.post(this.environment.baseUrl + '/api/logs/errors', {text})
.subscribe(/* handle http errors here*/);
}
}
}
}
This is pretty straightforward: receive an error text, check the environment, perform the respective action. But here are some problems I have with this solution:
logError
method is called. This is pretty useless, because after the app is built, the value of enviroment.name
never changes! The outcome of the switch
statement is always going to be the same, no matter how many times or in what situations we call this method.What do I suggest? Write separate LoggerServices
for each scenario, and inject only one of them conditionally, depending on the environment, using a factory
. Here’s how:
export class LoggerService {
logError(text: string): void { }
// maybe other methods like logWarning, or info
}
@Injectable()
class DevelopLoggerService implements LoggerService {
logError(text: string) {
console.error(text);
}
}
@Injectable()
class QALoggerService implements LoggerService {
constructor(
private http: HttpClient,
private environment: Environment,
) {}
logError(text: string) {
this.http.post(this.environment.baseUrl + '/api/reports', {text})
.subscribe(/* handle http errors here*/);
}
}
@Injectable()
class ProdLoggerService implements LoggerService {
constructor(
private http: HttpClient,
private environment: Environment,
) {}
logError(text: string) {
this.http.post(this.environment.baseUrl + '/api/logs/errors', {text})
.subscribe(/* handle http errors here*/);
}
}
A breakdown of what we have done:
LoggerService
down to just the declaration of it’s API, no implementation. We are going to use it as an injection token, and also as a hint for TS to know its interface.LoggerService
on each of them, so we have the same API interface.Okay, but how are we going to tell our components what version of the LoggerService
to use? Here is where factories
come in play.
A factory is a pure function which receives dependencies as arguments and returns a value which will be provided for a given token. Here is how we are going to provide our LoggerService
:
export function loggerFactory(environment: Environment, http: HttpClient): LoggerService {
switch (environment.name) {
case 'develop': {
return new DevelopLoggerService();
}
case 'qa': {
return new QALoggerService(http, environment);
}
case 'prod': {
return new ProdLoggerService(http, environment);
}
}
}
This is a function which will provide one of the Services depending on the runtime value of the environment variable, and do so once. Which is a very important thing, considering we wanted to reduce clutter in our code, and don’t perform unnecessary checkings.
But of course this is only part of the solution: we still need to tell Angular to use our factory and provide the necessary dependencies via the deps
array.
@NgModule({
providers: [
{
provide: LoggerService,
useFactory: loggerFactory,
deps: [HttpClient, Environment], // we tell Angular to provide this dependencies to the factory as arguments
},
{provide: Environment, useValue: environment}
],
// other metadata
})
export class AppModule { }
The greatest thing about this solution is that we don’t need to change anything else in our app: all the components that used the LoggerService
will continue to do so, as if the LoggerService
still contains the actual implementation.
export class SomeComponent implements OnInit {
constructor(
private logger: LoggerService,
) { }
ngOnInit() {
try {
// do something that may throw an error
} catch (error) {
this.logger.logError(error.message); // no need to change anything - works the same wy as previously
}
}
}
Angular makes sure that inside a given module, all components receive the same instance of a dependency. For example, if we provide a service on AppModule
, then declare a SomeComponent
on that module, and inject SomeService
into it, and also into AnotherComponent
declared in that same module, both SomeComponent
and OtherComponent
will receive the same instance of SomeService
. But in different modules it is not going to be the same: each module has its own dependency injector, and will spawn different instances of the same service for different modules. But what if we want the same instance for every component, service or whatever that tries to use our dependency? So we, essentially, want a Singleton.
We can have a singleton using the useFactory
again. At first we will have to implement a static getInstance
method on our class, just like with usual singletons, and then call it from out factory. Let’s say we want to implement a simple runtime data store, like very basic redux. I won’t go into details and only implement the getInstance
method:
export class StoreService {
// maybe store methods like dispatch, subscribe or others
private static instance: StoreService = null;
static getInstance(): StoreService {
if (!StoreService.instance) {
StoreService.instance = new StoreService();
}
return StoreService.instance;
}
}
Nothing fancy here, but we have to tell Angular to call our getInstance
method:
export function storeFactory(): StoreService {
return StoreService.getInstance();
}
@NgModule({
providers: [
{provide: StoreService, useFactory: storeFactory}
],
// other metadata
})
export class AppModule { }
And it’s done. Now we can just inject StoreService
into any component anywhere and be sure we have the same instance. We can also provide dependencies to the factory as usual.
Here are several short rules on how to work with DI in Angular:
Inject
decorator a string for it to look up the dependency. Never do that — a typo is always a possibility. Also, you cannot rely on IntelliSense to autocomplete it for you. Use an InjectionToken
instead.readonly
.implement
them like we did in the second example. This way if the replaced dependency’s interface changes, we will have to reimplement the class that is meant to replace it, so we don’t run into cryptic errors.This is only some part of the power that Angular’s Dependency Injection mechanism can give us. So it is very important to understand it thoroughly to write quality code.
Originally published by Armen Vardanyan at codeburst.io
#angular #web-development