Dependency Inversion in Angular
If you are new to Angular, you’ll be happy to know that it has its own dependency injection framework. This framework makes it easier to apply the dependency inversion principle, which is sometimes boiled down to the axiom “depend upon abstractions, not concretions.” In many programming languages, including TypeScript and C#, this could be translated by “depend upon interfaces, not classes.”
What Not to Do
So what does life look like without the warm glow of the dependency inversion principle? Let's have a look. The following AuthServiceImpl
class has a isLoggedIn
method that other classes in the app can depend upon:
class AuthServiceImpl {
constructor() {}
isLoggedIn() {
return true;
}
}
Now, an instance of AuthServiceImpl
can be created to make the isLoggedIn
method available to other classes in the app. Perhaps we have a HomeComponent
view that uses it to determine if it should display a login button:
class HomeComponent {
authService = new AuthServiceImpl();
constructor() {}
shouldDisplayLoginButton() {
return !this.authService.isLoggedin();
}
}
HomeComponent
creates and directly depends on AuthServiceImpl
and this is problematic and should be avoided for the following reasons:
- If we ever wanted to use a different implementation instead of
AuthServiceImpl
, we would have to modifyHomeComponent
. - If
AuthServiceImpl
itself had dependencies, they would have to be configured inside ofHomeComponent
. In any non-trivial project, with other components and services depending onAuthServiceImpl
, such configuration code would quickly become scattered across the app. - Good luck unit testing
HomeComponent
. We would normally use a mock or stubAuthServiceImpl
class, which is impossible to do here.
What to Do
Use dependency injection, that's what. Here's how it works in three simple steps:
- Use an interface to abstract the dependency implementation.
- Register the dependency with Angular's dependency injection framework.
- Angular injects the dependency into the constructor of whatever class that uses it.
So let's modify our code to make this happen.
Use an interface
First, we write an interface:
interface AuthService {
isLoggedIn(): boolean;
}
This interface is then implemented by our concrete class:
class AuthServiceImpl implements AuthService {
constructor() {}
isLoggedIn() {
return true;
}
}
Now, instead of depending directly on AuthServiceImpl
as we were before, our HomeComponent
will depend on the AuthService
interface.
class HomeComponent {
constructor(private authService: AuthService) {}
shouldDisplayLoginButton() {
return !this.authService.isLoggedin();
}
}
Notice how the new
keyword is gone from our class. That's what we generally want to see. Like Steve Smith points out in his blog post "new is glue" and we don't want to glue our HomeComponent
to a particular implementation of AuthService
.
Good, now we need to register our dependency with Angular's dependency injection framework.
Register the dependency with Angular
In this step, we need to tell Angular what to do when it is instantiating one of our classes and it notices that the constructor is expecting to be passed an AuthService
type.
Without a dependency injection framework, we would have to instantiate all of our app's classes manually. Something like this:
const authService = new AuthServiceImpl();
const homeComponent = new HomeComponent(authService);
This is an incredibly simple example, but imagine how much work this would be for a large app with tens, hundreds or thousands of classes.
Thankfully, Angular takes care of that for us. To make this happen, we first need to tell Angular that our AuthServiceImpl
is expected to be injected anywhere in our app and as such, we want it to be available for injection at the root level and below:
@Injectable()
class AuthServiceImpl implements AuthService {
constructor() {}
isLoggedIn() {
return true;
}
}
So we've now marked out AuthServiceImpl
as a service that can be injected, but Angular can't actually inject it anywhere until we configure an Angular dependency injector with a provider of that service. The most common place to do that is in the NgModule
that declares the component using our dependency. Let's do that:
@NgModule({
declarations: [HomeComponent],
providers: [
{ provide: AuthService, useClass: AuthServiceImpl }
],
})
export class HomeModule { }
And... this won't work. It looks like it should, but it won't. What we are telling Angular here is that, when it is instantiating a class that has a dependency of type AuthService
in its constructor, it should pass in an instance of AuthServiceImpl
. And this seems right, especially if you are used to other backend dependency injection frameworks like ASP.NET's where you would do something like
services.AddSingleton<IAuthService, AuthService>();
The big difference is that interfaces do not exist at runtime in JavaScript. They are a compile time construct of TypeScript. Therefore Angular's dependency injection system can't use an interface as a token at runtime. So how do we deal with this? We need to make a custom injection token:
import { InjectionToken } from '@angular/core';
export const AUTH_SERVICE = new InjectionToken<AuthService>('AuthService');
We can then use that token in the provider:
@NgModule({
declarations: [HomeComponent],
providers: [
{ provide: AUTH_SERVICE, useClass: AuthServiceImpl }
],
})
export class HomeModule { }
And in HomeComponent
using the Inject
property decorator :
class HomeComponent {
constructor(@Inject(AUTH_SERVICE) private authService: AuthService) {}
shouldDisplayLoginButton() {
return this.authService.isLoggedin();
}
}
Conclusion
Using Angular's powerful dependency injection system, it is possible to write decoupled, testable code, that follows the dependency inversion principle. Remember, depend on interfaces instead of concrete classes, and injection tokens are your friends.