HI WELCOME TO SIRIS

Why dependency injection

Leave a Comment

In this video we will discuss why should we use dependency injection and the benefits it provide. Let us understand this with a very simple example.


Let us say we want to build a Computer. In reality to build a computer we need several objects like processor, ram, hard-disk drive etc. To keep this example simple let's say we just need a processor object to build a computer.

Our Computer and Processor classes are as shown below. Notice at the moment, we are not using dependency injection. To build a Computer we need a Processor object and the Computer class is creating an instance of the Processor 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
  1. This code is difficult to maintain over time
  2. Instances of dependencies created by a class that needs those dependencies are local to the class and cannot share data and logic.
  3. Hard to unit test
export class Computer {

    private processor: Processor;

    constructor() {
        this.processor = new Processor();
    }
}

export class Processor {

    constructor() {
    }

}

Now let us understand why this code is difficult to maintain. Let us say, the Processor class needs to know the speed of the processor to be able to create an instance of it. One way to address this requirement is by passing the processor speed as a parameter to the constructor of the Processor class as shown below.

export class Processor {

    constructor(speed: number) {
    }

}

This change in the Processor class breaks the Computer class. So every time the Processor class changes, the Computer class also needs to be changed. At the moment, the Computer 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 Computer class may also need to be changed. Hence this code is difficult to maintain.

The reason we have this problem is because the Computer class itself is creating the instance of the Processor class. Instead if an external source can create the processor instance and provide it to the computer 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 for short as shown below.

export class Computer {

    private processor: Processor;

    constructor(processor: Processor) {
        this.processor = processor;
    }
}

export class Processor {

    constructor(speed: number) {
    }

}

Notice with DI, the Computer class is not creating the instance of the Processor class itself. Instead we have specified that the Computer class has a dependency on Processor class using the constructor. Now, when we create an instance of the Computer class, an external source i.e the Angular Injector will provide the instance of the Processor class to the Computer class. Since now the the Angular injector is creating the dependency instance, the Computer class need not change when the Processor 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 data and logic. The Processor class instance created in the Computer class is local to the Computer class and cannot be shared. Sharing a processor instance does not make that much sense, so let's understand this with another example. 

Let us say we have a service called UserPreferencesService which keeps track of the user preferences like colour, font-size etc. We want this data to be shared with all the other components in our application. Now if we create an instance of this UserPreferenceService class in every component class like we did in the Computer class, the service instance is local to the component in which we have created it and the data cannot be shared with other components. So if we need this UserPreferencesService in 10 different components, we end up creating 10 instances of the service, one for each component. As the service instance is local to the component that has created it, the data that local service instance has cannot be shared by other components. If this does not make sense at the moment, please do not worry, we will discuss it with a working example in our next video.

On the other hand if we use Dependency Injection (DI), the angular injector provides a Singleton i.e a single instance of the service so the data and logic can be shared very easily across all the components.

From unit testing standpoint, it is difficult to mock the processor object, so unit testing Computer class can get complex. In this example, the Computer class has just one dependency (i.e the dependency on the Processor object). 

In a real world application, a given object may have a dependency on several other objects, and those dependencies in turn may have dependencies on other objects. Just imagine, how complicated unit testing can become with all these hierarchies of dependencies if we do not have the ability to mock the dependencies.

With Dependency Injection it is very easy to mock objects when unit testing. This is one of the greatest benefits of DI.

If you are new to unit testing and mocking, it may be difficult for you to understand why unit testing can get difficult and complicated if we do not have the ability to mock dependencies. In our upcoming videos we will discuss unit testing and mocking and it should be much clear at that point.

So in summary DI provides these benefits
  1. Create applications that are easy to write and maintain over time as the application evolves
  2. Easy to share data and functionality as the angular injector provides a Singleton i.e a single instance of the service
  3. Easy to write and maintain unit tests as the dependencies can be mocked

0 comments:

Post a Comment

Note: only a member of this blog may post a comment.