DEV Community

Vitor Alves
Vitor Alves

Posted on

Why you should use dependency injection

As a software developer I've learned dependency injection (DI) longer than I'd like. After the first contact it was love at first sign.

In this text I'll explain some basics concept and how it will let you life easier.

First...what is a dependency?

In the world around you, a dependency is the state of existence of an entity or an item such that its stability is dictated by another entity or resource. For example, children are dependents on their parents to survive, plants are dependent on water and sunligth, and so on.

In the setting of coding, the definition of dependency shifts somewhat.

When class A uses some functionality of class B, then its said that class A has a dependency of class B. In Typescript, before we can use methods of other classes, we first need to create the object of that class. For example, you a have a controller class responsible to receive the signup request.

The SignUp controller tasks are:

  • Validate the fields from the request. Check if received name, email, password and password confirmation correctly.
  • Signup the user. Save the user data in the database.
  • Auth the user and send him a token.

In this example we see clearly that the signup class hast at least three dependencies. Without DI we could write our class like this:

class SignUpController{
  async handle (httpRequest: HttpRequest): Promise<HttpResponse> {
      const validation = new Validation() // First dependency
      const error = validation.validate(httpRequest.body)
      if (error) {
        .... return 400 Bad Request
      }

      const { name, email, password } = httpRequest.body

      const addAccount = new AddAccount() // Sencond dependency
      const account = await addAccount.add({ email, password, name })

      if (!account) {
        ... return 403 Forbidden
      }

      const authentication = new Authentication() // Third dependency
      const accessToken = await this.authentication.auth({ email, password })
      ...return 200 with a token JWT
  }
}
Enter fullscreen mode Exit fullscreen mode

What is dependency injection?

Dependency injection is a programming technique that makes a class independent of its dependencies.

In the code above we see that the dependencies are being initialized within the method. That is not a good way to code because it became your code less easy to change (more coupling) and more difficult to test.

Instead of create the objects of another classes we could receive the objects in the constructor of the SignUp class. Hence the name: Dependency Injection. It injects the objects inside the class.

class SignUpController
    constructor (
      private readonly addAccount: AddAccount,
      private readonly validation: Validation,
      private readonly authentication: Authentication
    ) { }

    ...

}
Enter fullscreen mode Exit fullscreen mode

Why does DI help with decoupling?

In the code above AddAccount, Validation and Authentication are interfaces. This allows our class to be independent of wich class will implement their respective methods. Our class don't care if we are using MongoDB or Postgres, if we are using a own class to validate or a third-party package.

So it's not necessary change our method if we decide to change some of the depencies implementation as long as it remains obeying the interface.

Why does DI help with testing?

I'll will explain this with example.

Imagine if we have to test if the method is returning 403 if the email provided is already in use.

Fisrt scenario - Not using DI

test('Should return 403 if email is already in use', async () => {
    const signUpController = new SignUpController()
    await signUpController .handle(makeFakeRequest('vitor@email.com')) // Creating the first user with the email
    const httpResponse = await signUpController.handle(makeFakeRequest('vitor@email.com')) // Trying to create the second user with the same email
    expect(httpResponse).toEqual(forbidden(new FieldInUseError('Email'))) // Expect to receive 403
})
Enter fullscreen mode Exit fullscreen mode

Second scenario - Using DI

test('Should return 403 if AddAccount returns null', async () => {
    const addAccountStub = new AddAccountStub() // Creating a stub class that implements the AddAccount interface
    const signUpController = new SignUpController(addAccountStub, ...) // It has to send the others dependencies too
    jest.spyOn(addAccountStub, 'add').mockReturnValueOnce(null) // Setting a null return on the method add of the addAccountStub object
    const httpResponse = await sut.handle(makeFakeRequest('vitor@email.com')) // Creating the user with the email
    expect(httpResponse).toEqual(forbidden(new FieldInUseError('Email')))
})
Enter fullscreen mode Exit fullscreen mode

In terms of quantity of code the first scenario seems to be shorter than the first. It's correct!! Using DI we have to write more code but we have more power to controll the returns of its dependencies. The main reason why DI helps to write unit test is because whe can mock the dependencies and control everything. If we are testing on method, we simply mock a return of another method to not disturb the test.

Conclusion

DI is not very easy to understand on the first glance for someone who don't use it because it's counterintuitive to write more code and increase the complexity but at the moment we start to write unit test, it is clear how important DI is. It decreases the necessity to know each detail of implementation of other classes dependencies.
It allows changes and become your code more reusable. In my point of view, DI it's essential in most cases.

Study more

Top comments (0)