Recently one of the .Net developer asked me the question "why we use class rather than interfaces while using Dependency Injection" the question was valid because as a .Net developer that was what we learned, Do not use or create a class instance directly.
Introduction
To be frank for me it's been more than a year I wrote .Net code, but I have written it for more than 8 years, I will copy the example from Microsoft documentation here, the below example shows how to create an instance, which is true for .Net as well as Angular.
public class IndexModel : PageModel
{
MyDependency _dependency = new MyDependency();
public async Task OnGetAsync()
{
await _dependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
Dependency Injection In .Net
The correct way to do this in .Net is
- Define an interface
public interface IMyDependency
{
Task WriteMessage(string message);
}
- Use the interface to create a new service
public class MyDependency : IMyDependency
{
private readonly ILogger<MyDependency> _logger;
public MyDependency(ILogger<MyDependency> logger)
{
_logger = logger;
}
public Task WriteMessage(string message)
{
_logger.LogInformation(
"MyDependency.WriteMessage called. Message: {MESSAGE}",
message);
return Task.FromResult(0);
}
}
- Register the interface and service
services.AddScoped<IMyDependency, MyDependency>();
- Using the DI in another class
public class IndexModel : PageModel
{
private readonly IMyDependency _myDependency;
public IndexModel(IMyDependency myDependency) {
_myDependency = myDependency;
}
public async Task OnGetAsync()
{
await _myDependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
The advantage of using the above approach is we can easily replace MyDependency with another class in the future and wherever we inject IMyDependency in we get the instance of that new Service, thus avoiding any direct dependency between the service and controller.
Dependency Injection In Angular
Angular has it's on DI framework, so there is no complication of registering an interface and related service as we saw in .Net implementation
We will be looking at Class providers for this article, to create a new service we can run
ng g service <service-name>
- The service code looks like below
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root',
})
export class LoginService {
constructor(public http: HttpClient) { }
login(user: any) {
console.log('login service called.');
}
}
I have left the login method blank just for visibility purpose, the service may have some HTTP calls as well.
- Using it in component
import { Component } from '@angular/core';
import { LoginService } from './login.service';
@Component({
selector: 'app-login',
template: `<button (click)="login()">Login<button>`
})
export class LoginComponent {
constructor(loginService: LoginService ) {
}
login() {
this.loginService.login('test');
}
}
The above component has loginService injected, but wait we are using the class name here rather than an interface as we do in .Net, if we are directly using the class how is it Dependency Injection, as we are using class here and why we are not using interface as it does exist in Typescript. Let's explore why and how next.
Why we cannot use Interface
The first question which comes to developers mind, who comes from .Net background is if interfaces exist why not to use interface rather than the class to achieve Dependency Injection. Let's see it programmatically why it is not possible.
- Install typescript using
npm i typescript -g
Create a Folder
InterfaceDemo
where we will create few filesOpen the folder with VS Code and run below command to create tsconfig.json
tsc -init
Open the
tsconfig.json
file and changetarget
toES2015
.Create a new file name ILogin.ts and copy the below code.
interface ILogin {
login(user:any): any;
}
- From terminal run below command.
tsc
- Now you will get ILogin.js file open the file and see the code it's
"use strict";
What just happened, do we mean whatever code we write inside interfaces does not exist once we get .js
files?
Yes, this is what happens, typescript is used mostly for type safety they do not exist once our code is compiled to JavaScript. Now we know, why we cannot use Interfaces for DI here.
How it is Dependency Injection if We are dependent on Class Instance
Now let's explore how we are not directly dependent on LoginService even though it is injected into the component.
- If you see the code we are not creating an instance of LoginService, we are injecting it, in the same way as we do with interface in .Net, Angular knows how to resolve the dependency, we don't need to create the instance even though injected service is dependent on another service like
HttpClient
in the mentioned example.
Ok agreed that we are not creating the instance, but what if I want to replace LoginService with another service let's call it as NewLoginService, we have to change the code again isn't it?
The answer is No, we don't need to let's see how we can achieve this.
- Create NewLoginService using CLI
ng g service NewLogin
- Add the below code into your service.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { LoginService } from './login.service';
@Injectable({
providedIn: 'root',
})
export class NewLoginService extends LoginService {
constructor(public http: HttpClient) { super(http); }
login(user: any) {
console.log('new login service called.');
}
}
Before you make next change run the app with LoginComponent and click on the Login button and see the console we should see
login service called
Now move to
app.module.ts
and you will notice providers property replace it with the below code.
providers: [{ provide : LoginService , useClass : NewLoginService }]
- Run the application and check the console again, and click on the Login button and see the console we should see
new login service called
.
This is how we can change the old service with a new service without changing even a single line of code, and this is how we can control which service should be available to the application.
Another advantage we get here if older service has 10 methods and we only want to replace 5 methods, add implementation for only 5 new methods in new service. For methods which do not exist in new service will be referred from old service, amazing isn't it?
Conclusion
The biggest mistake the developers who move to Angular make is they really get comfortable using typescript and feel home, though most of the concepts from C# is available, I always suggest developers read about Javascript as well it is really important, play with typescript try to see the output js
files whenever you get time. There is really a good post form @layzee
https://dev.to/layzee/sorry-c-and-java-developers-this-is-not-how-typescript-works-401
Top comments (8)
Great explanation. Got the same question from a colleague today so I was able to just show him this article 😁
Glad it was helpful.
Thanks for this great explanation !
In the last code sample did you forget to extend LoginService ?
export class NewLoginService extends LoginService
Thanks again.
Thanks buddy, yes I missed that.
Thanks!!
Thanks Santosh, that was very clear and helpful.
Thanks a lot 😊
I didn't notice that Javascript doesn't include the types until now. It makes sense now why Angular uses Classes for DI