When I first started programming dependency injection and all the concepts related to it, it seemed a bit abstract to me. I could still create services and inject them, but still there seemed to be a gap in my mind on how this implementation seemed to work. 

In this article, I will first explain what issues DI solves by starting with an example where DI has not been used. I will introduce the benefits of using DI as a coding pattern and how it makes programming easier. 

Code Without DI 

Let’s start with a simple example, we have a class called laptop which has two properties: ram and cpu. It is quite simple to declare it as below:

At this point, we can create a new instance of laptop pretty easily const laptop: Laptop  = new Laptop();  

Now let’s change the ram and cpu classes, supposing that ram needs a size and a unit to be initiated and cpu needs a model. 

As we can see, we need to make changes on laptop constructor, because when creating a new laptop we will need to provide cpu a model, ram a size and ram a unit:

const laptop: Laptop  = new Laptop('Intel', 8, 'GB');

Now as you can see, we used three parameters in this simple example. Imagine if we had to really define a new laptop. The number of parameters we would have to provide, how hard it would be at some point to manage it all—it would be a nightmare! Another drawback is that once we change the parameters needed to initialize cpu, for example, we would need to modify the laptop too. It would be hard to manage such code and it would be nigh impossible to test it. This background will make it easy for us now to understand the necessity of dependency injection. 

DI as a Design Pattern

According to Angular: “Dependency Injection is a coding pattern in which a class receives its dependencies from external sources rather than creating it itself.”  What this definition is basically saying is that since laptop has a dependency on cpu and ram, it will get the dependencies provided to it. This means that the laptop class will be as follows: 

To create a new laptop we will need to first create an instance of cpu and ram, as seen below:  

By providing the dependencies of cpu and ram to laptop, we solved the problem we noticed before. So if we add the generation to cpu, the laptop does not need to change; it will still just get cpu as a parameter:

Now we can see the advantages of providing the dependencies externally. Supposing that laptop will need cpu, ram, keyboard, etc. we will still have issues because we will need to create every dependency manually. Furthermore, in the application itself, it might be necessary to create lots of new instances, and to do so, we would need to copy-paste the code, which is definitely not a good practice.  

DI as a Framework 

At this point, we could create the new laptop instance by providing the dependencies externally ourselves. Now, let’s take it one step further and allow Angular to create these dependencies when needed; we will just provide the default values which can be customized according to our needs. Let’s start with cpu

By using @Inject(), we are telling Angular that cpu has a dependency on the model and whenever the cpu class is used, Angular DI Framework will provide the dependency to us. We will do the same thing for ram by injecting the same parameters used in cpu. Now we need to provide them. For now, I will just provide them inside app module providers:

Now it’s very easy to create a new laptop:

By doing this, we are injecting cpu and ram and then we are creating a new instance of laptop. If we check in the browser, this is the instance we’ve created: 

Laptop instance created

So Angular DI Framework has automatically created a new instance of cpu and ram using the values we provided successfully. 

Let’s consider the case where we have two modules, and when we create a new laptop of each of the modules, it creates two different laptops such as Module A: Intel Laptop and Module B: AMD Laptop

In this case, we need to tell Angular that when each of the modules is initiated, it should provide different values for model, generation, ram size, and unit. These are the values that we inject into cpu and ram

In Module A these will be the providers:

providers: [        
    {provide: 'model', useValue: 'AMD'},         
    {provide: 'generation', useValue: 'Radeon'},        
    {provide: 'size', useValue: 32},        
    {provide: 'unit', useValue: 'GB'}            

And in Module B

 providers: [        
    {provide: 'model', useValue: 'Intel'},         
    {provide: 'generation', useValue: 'i9-9900K'},        
    {provide: 'size', useValue: 16},        
    {provide: 'unit', useValue: 'GB'}       

If we create a laptop inside each of the modules, we will see the following when we load them in the browser: 

Module A Laptop

Module B Laptop

So at this point, we have successfully injected ram and cpu in each of the components and provided different values that Angular DI injected at our A component and B Component.


Dependency Injection design pattern is when you provide the instance dependencies externally. Dependency Injection framework is when you define the dependencies and Angular automatically provides them to components. When speaking of Dependency Injection, it is most often related to services, but in the example that I chose, I hope I made it clear that the concept is quite straightforward: One class has a dependency which can be a string/number/boolean parameter, service or another class and by properly configuring it, Angular Injector will provide that when it is needed. 

In my next article, I will talk more in-depth regarding these services, how hierarchical injector works, and different options to configure dependencies such as useClass, useValue, useExisting and so on.

Source link

Write A Comment