Table of contents
What is Depdency Injection?
Dependency injection is basically providing the objects that an object needs (its dependencies) instead of having it construct them itself.
An analogy to understand dependency injection is constructing a home. When building a home, we don't make every component from scratch. For example, instead of constructing a door ourselves, we rely on a specialized door supplier to provide a pre-built door that fits our requirements. The door is a dependency of the home construction process, and we inject it into the home.
Similarly, in software development, we identify the dependencies of a class or module and inject them from the outside. This allows for more flexibility, easier testing, and better separation of concerns.
// Door class
class Door {
constructor(color) {
this.color = color;
}
open() {
console.log(`The ${this.color} door is opened.`);
}
}
// House class with dependency injection
class House {
constructor(door) {
this.door = door;
}
enter() {
console.log("You entered the house.");
this.door.open();
}
}
// Creating instances and injecting dependencies
const door = new Door("red"); // Dependency
const house = new House(door); // Dependency injection
// Entering the house
house.enter();
One problem that can arise with dependency injection is the increased complexity and potential for managing a large number of dependencies within a class. If a class depends on a significant number of objects (e.g., 10, 20, or more), it can become cumbersome to manually provide and manage all the dependencies.
To address this issue, Dependency Injection (DI) Containers, like the one provided by Angular, can help simplify the management of dependencies in a more automated and centralized manner.
DI Container
A DI container is a tool used to manage dependencies in a software application. It provides a mechanism to register dependencies and, when requested, creates and returns instances of those dependencies*. The DI container acts as a central hub for dependency management, facilitating the retrieval and instantiation of dependencies throughout the application.*
Angular DI Container
In Angular, the Dependency Injection (DI) system is an integral part of the framework and is powered by Angular's built-in DI container.
To use the Angular DI container, you can follow these steps:
Step 1: Register the Dependency wiht a Token and Provide the Resolution Configuration
In this step, we register the dependency and provide the necessary configuration for resolving and instantiating the dependency.
To register a depdency Angular DI Container expects two things: a Token and the Depdency Resolution Configuration
Option 1: Using @Injectable()
import { Injectable } from '@angular/core';
@Injectable()
export class DoorService {
// ...
}
In this example, we register the DoorService
class dependency using the @Injectable()
decorator. By marking the class with @Injectable()
, Angular treats the class itself as a token for registration and resolution. When the DoorService
dependency is requested, Angular's DI system automatically resolves and provides an instance of DoorService
. This approach is suitable when the token (i.e., class name) itself can be used for registration and resolution.
Option 2: Using the providers
Array
Class Providers:
import { NgModule } from '@angular/core';
import { MyService } from './my-service';
@NgModule({
providers: [
DoorService
// ... other dependencies
]
})
export class DoorService {
// ...
}
In this example, we register the DoorService
class dependency using the providers
array within the @NgModule
decorator. By including DoorService
in the providers
array, we provide the configuration for resolving and instantiating the dependency. When the DoorService
dependency is requested within the module or its child components, Angular's DI system resolves and provides an instance of DoorService
based on the provided configuration.
Note: When registering a class as a dependency, if the token (class name) itself is used for registration, Angular automatically resolves the dependency based on the class. In such cases, there's no need to explicitly provide a useClass
configuration in the provider array.
Value Providers:
When registering a class as a dependency, Angular automatically resolves and instantiates it. No additional configuration is required. Here's an example:
import { NgModule } from '@angular/core'; @NgModule({ providers: [{ provide: 'API_KEY', useValue: 234234 }] }) export class MyModule { // ... }
In this example, Inside the
providers
array of theNgModule
decorator, we provide the'API_KEY'
token using theuseValue
property and assign it the value ofAPI_KEY
. This configuration allows Angular's DI system to resolve the'API_KEY'
token and provide it with the value'234234'
when requested.Using useFactory Provider:
The
useFactory
provider allows us to register a dependency by providing a factory function that creates and returns an instance of the dependency.import { NgModule } from '@angular/core'; @NgModule({ providers: [ { provide: 'DoorService', useFactory: () => ({ color: 'red', ... }) } ] }) export class MyModule { // ... }
In this example, we define a factory function using the
useFactory
property. The factory function does not have any parameters and returns an object with properties, such ascolor: 'red'
. The factory function is assigned as the provider for the'DoorService'
token using theprovide
property.When the
'DoorService'
dependency is requested, Angular's DI system calls the factory function and provides the returned object as the instance of the'DoorService'
dependency. In this case, the returned object will have the properties defined in the factory function, such ascolor: 'red'
.Using UseExisting Provider:
The
useExisting
provider allows us to register a dependency using an alias for another provider.import { NgModule } from '@angular/core'; @NgModule({ providers: [ { provide: 'AliasDependency', useClass: DoorService } ] }) export class MyModule { // ... }
In this modified example, we have replaced the
MyDependency
class withDoorService
. Now, when the'AliasDependency'
dependency is requested, Angular's DI system resolves it to theDoorService
class and provides an instance ofDoorService
.
Step 2: Request Depdency
When you have a dependency registered with the same token as the class name, Angular's DI system can automatically inject the dependency for you. However, if the token name is different, such as when using value, factory, or alias providers, you need to explicitly specify the dependency using the @Inject()
decorator.
Example 1: Dependency with the Same Token
@Injectable()
export class DoorService {
// ...
}
@Component({
// ...
})
export class MyComponent {
constructor(private doorService: DoorService) {
// doorService is automatically injected
// ...
}
}
Example 2: Dependency with a Different Token
@Injectable()
export class DoorService {
// ...
}
@Component({
// ...
})
export class MyComponent {
constructor(@Inject('AliasDependency') private doorService: DoorService) {
// doorService is explicitly requested using the 'AliasDependency' token
// ...
}
}
By using the @Inject()
decorator, you can specify the token for dependencies that are registered with different tokens, ensuring that Angular's DI system resolves and provides the correct dependency instance for your component or service.