Dependency Injection is a coding pattern in which a class receives its dependencies from an external source rather than creating a new one.
Why should we use Dependency Injection?
Let us assume we want to build a house. To build a house, we need several objects like bricks, wood, concrete, iron etc. To keep this example simple, let's say we just need a "bricks" object to build the house.
Our "House" and "Bricks" classes are shown below. Notice that now, we are not using dependency injection. To build a house, we need a Bricks object and the House class is creating an instance of the Bricks class it needs. This is the kind of programming style that most of us are used to and it is easy to understand as well. But there are 3 fundamental problems with this code.
- This code is difficult to maintain over time
- Instances of the dependencies created by a class that needs those dependencies are local to the class and cannot share the data and the logic.
- Hard to unit test.
- export class House {
- private bricks: Bricks;
- constructor() {
- this.bricks = new Bricks();
- }
- }
- export class Bricks {
- constructor() {}
- }
Now, let us understand why this code is difficult to maintain. Let us say the Bricks class needs to know the size of the bricks to be able to create an instance of it. One way to address this requirement is passing the brick size as a parameter to the constructor of the Bricks class, as shown below.
- export class Bricks {
- constructor(size: number) {}
- }
This change in the Bricks class breaks the House class. So, every time the Bricks class changes, the House class also needs to be changed. At this moment, the House class has only one dependency. In reality, it may have many dependencies and those dependencies, in turn, may have other dependencies. So, when any of these dependencies change, the House class may also need to be changed. Hence, this code is difficult to maintain.
The reason we have this problem is that the House class itself is creating the instance of the Bricks class. Instead, if an external source can create the Bricks instance and provide it to the House class, then this problem can be very easily solved and that's exactly what dependency injection does. I have rewritten the above code using dependency injection (DI), as shown below.
- export class House {
- private bricks: Bricks;
- constructor(bricks: Bricks) {
- this.bricks = bricks;
- }
- }
- export class Bricks {
- constructor(size: number) {}
- }
Notice that with Dependency Injection, the House class is not creating the instance of the Bricks class itself. Instead, we have specified that the House class has a dependency on Bricks class using the constructor. Now, when we create an instance of the House class, an external source, i.e., the Angular Injector will provide the instance of the Bricks class to the House class. Since now the Angular injector is creating the dependency instance, the House class needs not change when the Bricks class changes.
Now, let us understand the second problem - Instances of dependencies created by a class that needs those dependencies are local to the class and cannot share the data and logic. The Bricks class instance created in the House class is local to the House class and cannot be shared. Sharing a bricks instance does not make that much sense.
Dependency Injection provides benefits as mentioned below -
- Create applications that are easy to write and maintain over time as the application evolves.
- Easy to share data and functionality because the Angular injector provides a Singleton, i.e., a single instance of the service.
- Easy to write and maintain unit tests as the dependencies can be mocked.