logo
Published on

Dependency Injection

Authors
covers2-3

Dependency Injection? Dependency Inversion? Inversion of Control? IoC container? 😵‍💫

The first time I heard about dependency injection was at the beginning of my career when I learned Angular, and the term dependency injection was for me: ״the thing that happens when I put stuff in the constructor״ 🙄

The second time I had to deal with dependency injection was when a team leader at one of my previous jobs asked me to watch a video that explained how to build an IoC container. And I won't say any more because he is probably reading this 😏

The third round with dependency injection was when I started to learn NestJS, but this time I decided to learn and understand it deeply 🤓

So, after I threw emojis all over the place and got your attention, let's get started with the 3 terms that, although related, are commonly confused and misinterpreted:

Inversion of Control (IoC) - means that the control flow of a program is inverted: instead of the programmer controlling the flow of a program, the external sources (framework, services, other components) take control of it.

In traditional procedural programming, the code that controls the execution of the program, the main function, instantiates objects, and calls methods. With IoC, it is a framework that does the instantiation, method calls and triggers user actions, having full control of the flow and removing this responsibility from the main function.

An example of this is an IoC container, whose purpose is to inject objects into client code without having the client code specify the concrete implementation.

Dependency Inversion Principle (DIP) - a design principle, one of the SOLID principle that was conceived by Robert Martin, a.k.a. Uncle Bob. This principle suggests two things:

  1. High-level modules should not depend on low level modules, and both should depend on abstraction.
  2. Abstractions should not depend on details, and details should depend on abstractions.

And now, with simpler words: instead of saying “I want class A”, say: “I want something that implements interface A”. This means we depend on interfaces, and the interface methods naming should be abstract, for example: openFile() and not openExcel();

Dependency Injection - a desing pattern. the idea is that a class should requests dependencies from external sources rather than creating them. So, in general we can say that DI is a more specific version of IoC pattern, where implementations are passed into an object through constructors / setters / service lookups, which the object will depend on in order to behave correctly.

There are 3 types of Dependecy Injection:

  1. Constructor Injection: the dependencies are declared as parameters of the constructor. As a result, you cannot create a new instance of the class without passing in a variable of the type required by the constructor.
  2. Property Injection (Setter Injection): passing an object of the dependent class through a public property of the client class.
  3. Method Injection (Interface Injection): allows you to inject a dependency right at the point of use, so that you can pass any implementation you want without having to worry about storing it for later use.
enough-blah-blah

What could be more useful than first showing you what a dependency injection is NOT. For that, let's take a very unordinary example, one that has never been used before while explaining concepts of OOP: Car and Engine

import { Engine } from './engine'

class Car {
  private engine: Engine

  public constructor() {
    this.engine = new Engine()
  }

  public startEngine(): void {
    this.engine.fireCylinders()
  }
}

In this example, the Car class is instantiating the Engine inside the Car constructor (i.e. call the new operator on Engine). In other words, the Car class is not only responsible of its functionality, but also responsible for instantiating the Engine.

Now, we are going to have a look at the same class, but with a DI approach

interface IEngine {
  fireCylinders: () => void
}
class Engine implement IEngine {
  fireCylinders(): void {
    // do something
  }
}
import { IEngine } from './engine.interface'

class Car {
  public constructor(private engine: IEngine) {}

  public startEngine(): void {
    this.engine.fireCylinders()
  }
}

What are the differences and why should I care?

As opposed to the first example, in this example the Car class has an instance of Engine passed in (injected) from a higher level of control into its constructor. This instance is actually an interface and NOT a concrete implementation. The benefits of this is that we can pass any engine we want that satisfies the Engine interface. The car shouldn't be aware whether it is a Mazda / Peugeot / Chevrolet / Tesla engine. This leads us to another benefit - unit testing. It is way easier to test the Car class now, as we just have to mock a service that implements the Engine Interface.

As a side note, I want to clarify here that using dependecies that were defined by interfaces is a little bit challanging in TS, and we will see later how it can be done in NestJS.

Dependency Injection In NestJS:

what is NestJS? (From NestJS documentation)

Nest (NestJS) is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with and fully supports TypeScript (yet still enables developers to code in pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

Concepts NestJS regarding DI:

  1. Providers - services, repositories, factories, helpers can be treated as providers that can be injected as a dependencies.
  2. Scopes - at bootstrap, the injector (IoC container) of NestJS, resolvs all the dependencies. In other words, it instantiates the providers and creates all required dependencies and gives us the instances. Providers are synchronized with the application's lifecycle, but there are ways to make them request-scoped.
  • DEFAULT scope - the injector is holding the created dependencies instances and reuses them if needed. When the application is closed, each provider is destroyed.
  • REQUEST scope - on each incoming request, a new provider instance is created. This instance is garbage-collected after the request has completed.
  • TRANSIENT scope - each consumer of the transient provider will receive a new dedicated instance.
  1. How to use DI in NesJS?
  • Decorate a provider (in our case, service) with the @Injectable() decorator, indicating that the service is managed by the Nest IoC Container.
@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  getUsers(): User[] {
    return this.userRepository.findAll();
  }
  • Declare a dependency in UserController via constructor injection:
@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}

  @Get()
  async getUsers(): Promise<User[]> {
    return this.userService.getUsers()
  }
}
  • Associate UserService with UserController in UserModule:
@Module({
  controllers: [UserController],
  providers: [UserService, UserRepository],
})
export class UserModule {}

Nest DI container holds 2 containers of data:

  1. List of classes and their dependencies:
  • UserService => UserRepository, UserRepository => (doen't have a dependency)
  1. List of instances that were created:
  • UserService and UserRepository

As a result, we are getting back an initialized UserController.

The following diagram describes how DI container in Nest works:

nest-di
  1. Custom Provider - As I mentioned earlier, in TS it is a little bit challanging to use dependecies that are defined by interfaces. If we want to define UserRepository as TypeScript interface, we need to know that it will be erased in transpilation phase and won’t be available in runtime. This is the main difference between statically typed languages and dynamically typed ones. TypeScript here is just like a syntactic sugar which makes us feel more confident about what we are writing. NestJS has several ways to configure our dependencies, and since we want to use UserRepository as TypeScript interface, we can do it with:

Non-class-based provider tokens:

  1. Define IUserRepository interface:
export const USER_REPOSITORY_TOKEN = 'USER_REPOSITORY_TOKEN'

interface IUserRepository {
  findAll: () => Promise<User[]>
  findOne: (id: string) => Promise<User>
  create: (user: User) => Promise<{ id: string }>
  update: (id: string) => Promise<User>
  remove: (id: string) => Promise<boolean>
}
  1. create UserRepository class that implements IUserRepository interface:
@Injectable()
export class UserRepository implements IUserRepository {
  constructor() {}

  async findAll(): Promise<User[]> {}

  async findOne(): Promise<User> {}

  async create(): Promise<{ id: string }> {}

  async update(): Promise<User> {}

  async remove(): Promise<boolean> {}
}
  1. Registering UserRepository service as a provider
@Module({
  imports: [],
  providers: [
    {
      provide: USER_REPOSITORY_TOKEN,
      useClass: UserRepository,
    },
    UserService,
  ],
})
export class UserModule {}
  1. Inject UserRepository into UserService:
@Injectable()
export class UserService {
  constructor(@Inject(USER_REPOSITORY_TOKEN) private userRepository: IUserRepository) {}

  getUsers(): User[] {
    return this.userRepository.findAll()
  }
}

A lot of time has passed since you read the first line of this blog post, so I will try to explain what happened in the blocks of code above as simple as I can. The @Inject decorator, along with the USER_REPOSITORY_TOKEN token help the DI container to perform a lookup mechanism.

It basically says:

"Hey, I don't know what to do with the interface, I need a concrete implementation. Please, Give me somehting that implements the IUserRepository interface".

And gets an answer:

"Of course, I have one. Here, take the UserRepository service".

Conclusions:

  1. Dependency injection is confusing and it is totally OK to think about it as ״the thing that happens when I put stuff in the constructor״.
  2. Inject an interface and not a concrete implementation. It will make your life easier in unit tests.
  3. TS has several limitations with injecting an interface but NestJS has resolved it with non-class-based provider tokens injection.