Implementing Dependency Inversion in Angular Using Injection Tokens: A Complete Guide
Introduction
Dependency Inversion Principle (DIP) is one of the five SOLID principles of software design that promotes loose coupling and enhances code maintainability. In Angular, DIP is implemented through Dependency Injection (DI), making the framework modular and scalable.
In this article, we'll explore what Dependency Inversion is, why it's important in Angular, and how to implement it using Injection Tokens. We'll also cover its pros and cons, ensuring a balanced perspective.
What is Dependency Inversion Principle (DIP)?
DIP states that high-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces or abstract classes).
Traditional Approach (Tightly Coupled Code)
class UserService {
getUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}
class UserComponent {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
showUsers() {
console.log(this.userService.getUsers());
}
}
Problem: The UserComponent directly depends on UserService, making it difficult to replace UserService with a different implementation (e.g., fetching data from an API instead of a static list).
Implementing Dependency Inversion in Angular Using Injection Tokens
Step 1: Create an Interface (Abstraction)
export interface IUserService {
getUsers(): string[];
}
Step 2: Define an Injection Token
import { InjectionToken } from '@angular/core';
import { IUserService } from './user-service.interface';
export const USER_SERVICE_TOKEN = new InjectionToken<IUserService>('USER_SERVICE_TOKEN');
Step 3: Implement the Interface
import { Injectable } from '@angular/core';
import { IUserService } from './user-service.interface';
@Injectable()
export class UserService implements IUserService {
getUsers(): string[] {
return ['Alice', 'Bob', 'Charlie'];
}
}
Step 4: Provide the Dependency Using Injection Token (For NgModule-based Apps)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { UserComponent } from './user.component';
import { UserService } from './user.service';
import { USER_SERVICE_TOKEN } from './user-service.token';
import { IUserService } from './user-service.interface';
@NgModule({
declarations: [AppComponent, UserComponent],
imports: [BrowserModule],
providers: [{ provide: USER_SERVICE_TOKEN, useClass: UserService }],
bootstrap: [AppComponent]
})
export class AppModule {}
Step 5: Inject the Dependency in a Component
import { Component, Inject } from '@angular/core';
import { USER_SERVICE_TOKEN } from './user-service.token';
import { IUserService } from './user-service.interface';
@Component({
selector: 'app-user',
template: `<div *ngFor="let user of users">{{ user }}</div>`
})
export class UserComponent {
users: string[];
constructor(@Inject(USER_SERVICE_TOKEN) private userService: IUserService) {
this.users = this.userService.getUsers();
}
}
Implementing Dependency Inversion in Standalone Components
With Angular's standalone components, the approach is slightly different as there is no NgModule. Instead, the provider is registered directly in the component or the application bootstrap.
Recommended by LinkedIn
Step 1: Provide Dependency in Standalone Component
import { Component, Inject, Injectable, InjectionToken } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
// Define an interface
export interface IUserService {
getUsers(): string[];
}
// Create an Injection Token
export const USER_SERVICE_TOKEN = new InjectionToken<IUserService>('USER_SERVICE_TOKEN');
// Implement the service
@Injectable()
export class UserService implements IUserService {
getUsers(): string[] {
return ['Alice', 'Bob', 'Charlie'];
}
}
// Define the Standalone Component
@Component({
selector: 'app-user',
standalone: true,
template: `<div *ngFor="let user of users">{{ user }}</div>`,
providers: [{ provide: USER_SERVICE_TOKEN, useClass: UserService }]
})
export class UserComponent {
users: string[];
constructor(@Inject(USER_SERVICE_TOKEN) private userService: IUserService) {
this.users = this.userService.getUsers();
}
}
// Bootstrap the application
bootstrapApplication(UserComponent);
This ensures that the service is provided at the component level, keeping the dependency injection localized.
Pros and Cons of Using Dependency Inversion with Injection Tokens in Angular
✅ Pros
❌ Cons
Conclusion
Implementing Dependency Inversion in Angular using Injection Tokens helps in writing scalable, maintainable, and testable applications. Whether using NgModule or Standalone Components, applying this principle ensures better modularity and flexibility.
By following the principles outlined in this article, you can build Angular applications that are extensible, loosely coupled, and future-proof. 🚀
💡 What’s Next?
Want to dive deeper? Explore:
Follow me on:
Be sure to clap and follow the writer ️👏
Cheers to cleaner, better code!!!
And, if you found this article helpful, a clap and a follow would be much appreciated 😊😊
Happy Coding! 🎯