Angular Dependency Injection Explained

When I first started programming dependency injection and all the concepts related to it seemed to be a bit abstract for 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, first I will explain what are the issues DI had to solve by starting with an example where we DI is not used. Step by step 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 te declare it as below:

export class Cpu {
 
    constructor() {}
}
export class Ram {
 
    constructor() {}
}
export class Laptop {
    cpu: Cpu;
    ram: Ram;
    
    constructor() {
        this.cpu = new Cpu();
        this.ram = new Ram();
    }
 
}

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

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

export class Ram {
    size: number;
    unit: string;
 
    constructor(size: number, unit: string) {
        this.size = size;
        this.unit = unit;
    }
}
export class Cpu {
    model: string;
 
    constructor(model: string) {
        this.model = 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 model, ram size and ram unit.  const laptop: Laptop  = new Laptop(‘Intel’, 8, ‘GB’);

export class Laptop {
    cpu: Cpu;
    ram: Ram;
    
    constructor(cpuModel: string, ramSize: number, ramUnit: string) {
        this.cpu = new Cpu(cpuModel);
        this.ram = new Ram(ramSize, ramUnit);
    }
}

Now as we can see, we need three parameters in this simple example. Imagine if we had to really define a new laptop the amount of parameters we should provide and hard it will be at some point to manage it all. Another drawback is that once we change the parameters needed to initialize Cpu for example, we will need to modify the Laptop too. If we add a generation parameter to Cpu, we will need to update the laptop as well. It will be hard to manage such code. On the other hand it would be impossible to test it. This background will make it easy for us now to understand a bit more 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 a laptop has a dependency on cpu and ram, it will get these dependencies provided to it. Which means that laptop class will be as follows: 

export class Laptop {
    cpu: Cpu;
    ram: Ram;
    
    constructor(cpu: Cpu, ram: Ram) {
        this.cpu = cpu;
        this.ram = ram;
    }
}

To create a new laptop we will need to first create an instance of Cpu and Ram as below: 

const cpu: Cpu = new Cpu('Intel');       
const ram: Ram = new Ram(8,'GB');     
const laptop: Laptop  = new Laptop(cpu, ram);

By providing the dependencies: 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. 

const cpu: Cpu = new Cpu('Intel', 'i9-9900K');
const ram: Ram = new Ram(8,'GB');
const laptop: Laptop  = new Laptop(cpu, ram);

Now we can see the advantages of providing the dependencies externally. Supposing that laptop will need cpu, ram, keyboard..ect still we will have issues because we will need to manually create a ton of dependencies manually. Furthermore in the application it might be necessary to create lots of new instances and we will just need to copy paste the code which is definitely not a good practise.  

  • DI as a framework 

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

@Injectable()
export class Cpu {
 model: string;
 generation: string;
 
 constructor(@Inject('model') model: string, @Inject('generation') generation:  string)   {
        this.model = model;
        this.generation = generation;
 }
}

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

providers: [   
      Cpu,
      Ram,
      {provide: 'model', useValue: 'Intel'}, 
      {provide: 'generation', useValue: 'i9-9900K'},
      {provide: 'size', useValue: 8},
      {provide: 'unit', useValue: 'GB'}      
  ],

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

 constructor(private cpu: Cpu, private ram: Ram) {
      this.createLaptop();
  }
 
  createLaptop(): void {
      const laptop: Laptop  = new Laptop(this.cpu, this.ram);
      console.log('Laptop: ', laptop);
  }

So we are injecting Cpu and Ram and then we are creating a new instance of laptop. And if we check in the browser this is the instance we 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 each of the modules create a different laptop 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 in CPU and RAM. 

In Module A this will be the providers:

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

And in module B: 

 providers: [
        Ram, Cpu,
        {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, when we load them in the browser we will see: 

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.

  • Conclusions

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 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 the next article I will get more in depth regarding the services, how hierarchical injector works and different options to configure dependencies such as useClass, useValue, useExisting and so on. 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s