My TypeScript Software Design & Architecture book just prelaunched! Check out solidbook.io.
Close

Dependency Injection & Inversion Explained | Node.js w/ TypeScript

Sep 11th, 2019 / 10 min read / Share / Edit on GitHub
Dependency Injection and Depedency Inversion are two related but commonly misused terms in software development. In this article, we explore both types of DI and how you can use it to write testable code.

This topic is taken from Solid Book - The Software Architecture & Design Handbook w/ TypeScript + Node.js. Check it out if you like this post.

One of the first things we learn in programming is to decompose large problems into smaller parts. That divide-and-conquer approach can help us to assign tasks to others, reduce anxiety by focusing on one thing at a time, and improve modularity of our designs.

But there comes a time when things are ready to be hooked up.

That's where most developers go about things the wrong way.

Most developers that haven't yet learned about the solid principles or software composition, and proceed to write tightly couple modules and classes that shouldn't be coupled, resulting in code that's hard to change and hard to test.

In this article, we're going to learn about:

  • Components & software composition
  • How NOT to hook up components
  • How and why to inject dependencies using Dependency Injection
  • How to apply Dependency Inversion and write testable code
  • Considerations using Inversion of Control containers

Terminology

Let's make sure that we understand the terminology on wiring up dependencies before we continue.

Components

I'm going to use the term component a lot. That term might strike a chord with React.js or Angular developers, but it can be used beyond the scope of web, Angular, or React.

A component is simply a part of an application. It's any group of software that's intended to be a part of a larger system.

The idea is to break a large application up into several modular components that can be independently developed and assembled.

The more you learn about software, the more you realize that good software design is all about composition of components.

Failure to get this right leads to clumpy code that can't be tested.

Dependency Injection

Eventually, we'll need to hook components up somehow. Let's look at a trivial (and non-ideal) way that we might hook two components up together.

In the following example, we want to hook up a UserController so that it can retrieve all the User[]s from a UserRepo (repository) when someone makes an HTTP GET request to /api/users.

repos/userRepo.ts
/**
 * @class UserRepo
 * @desc Responsible for pulling users from persistence.
 **/

export class UserRepo {
  constructor () {}

  getUsers (): Promise<User[]> {
    // Use Sequelize or TypeORM to retrieve the users from
    // a database.
  }
}

And the controller...

controllers/userController.ts
import { UserRepo } from '../repos' // Bad

/**
 * @class UserController
 * @desc Responsible for handling API requests for the
 * /user route.
 **/

class UserController {
  private userRepo: UserRepo;

  constructor () {
    this.userRepo = new UserRepo(); // Also bad, read on for why
  }

  async handleGetUsers (req, res): Promise<void> {
    const users = await this.userRepo.getUsers();
    return res.status(200).json({ users });
  }
}

In the example, we connected a UserRepo directly to a UserController by referencing the name of the UserRepo class from within the UserController class.

This isn't ideal. When we do that, we create a source code dependency.

Source code dependency: When the current component (class, module, etc) relies on at least one other component in order to be compiled. Source code depdendencies should be limited.

The problem is that everytime that we want to spin up a UserController, we need to make sure that the UserRepo is also within reach so that the code can compile.

The UserController class depends directly on the UserRepo class.

When might you want to spin up an isolated UserController?

During testing.

It's a common practice during testing to mock or fake dependencies of the current module under test in order to isolate and test different behaviors.

Notice how we're a) importing the concrete UserRepo class into the file and b) creating an instance of it from within the UserController constructor?

That renders this code untestable. Or at least, if UserRepo was connected to a real live running database, we'd have to bring the entire database connection with us to run our tests, making them very slow...


Dependency Injection is a technique that can improve the testability of our code.

It works by passing in (usually via constructor) the dependencies that your module needs to operate.

If we change the way we inject the UserRepo from UserController, we can improve it slightly.

controllers/userController.ts
import { UserRepo } from '../repos' // Still bad

/**
 * @class UserController
 * @desc Responsible for handling API requests for the
 * /user route.
 **/

class UserController {
  private userRepo: UserRepo;

  constructor (userRepo: UserRepo) { // Better, inject via constructor
    this.userRepo = userRepo; 
  }

  async handleGetUsers (req, res): Promise<void> {
    const users = await this.userRepo.getUsers();
    return res.status(200).json({ users });
  }
}

Even though we're using dependency injection, there's still a problem.

UserController still relies on UserRepo directly.

This dependency relationship still holds true.

Even still, if we wanted to mock out our UserRepo that connects to a real SQL database for a mock in-memory repository, it's not currently possible.

UserController needs a UserRepo, specifically.

controllers/userRepo.spec.ts
let userController: UserController;

beforeEach(() => {
  userController = new UserController(
    new UserRepo() // Slows down tests, needs a db running
  )
});

So.. what do we do?

Introducing the Dependency Inversion Principle!

Dependency Inversion

Dependency Inversion is a technique that allows us to decouple components from one another. Check this out.

What direction does the flow of dependencies go in right now?

From left to right. The UserController relies on the UserRepo.

OK. Ready?

Watch what happens when we slap an interface in between the two components make UserRepo implement an IUserRepo interface, and then point the UserController to refer to that instead of the UserRepo concrete class.

repos/userRepo.ts
/**
 * @interface IUserRepo
 * @desc Responsible for pulling users from persistence.
 **/

export interface IUserRepo {          // Exported
  getUsers (): Promise<User[]>
}

class UserRepo implements IUserRepo { // Not exported
  constructor () {}

  getUsers (): Promise<User[]> {
    ...
  }
}

And update the controller to refer to the IUserRepo interface instead of the UserRepo concrete class.

controllers/userController.ts
import { IUserRepo } from '../repos' // Good!

/**
 * @class UserController
 * @desc Responsible for handling API requests for the
 * /user route.
 **/

class UserController {
  private userRepo: IUserRepo; // like here

  constructor (userRepo: IUserRepo) { // and here
    this.userRepo = userRepo;
  }

  async handleGetUsers (req, res): Promise<void> {
    const users = await this.userRepo.getUsers();
    return res.status(200).json({ users });
  }
}

Now look at direction of the flow of dependencies.

You see what we just did? By changing all of the references from concrete classes to interfaces, we've just flipped the dependency graph and created an architectural boundary inbetween the two components.

Design principle: Program against interfaces, not implementations.

Maybe you're not as excited about this as I am. Let me show you why this is so great.

And if you like this article so far, you might like my book, "Solid - The Software Architecture & Design Handbook w/ TypeScript + Node.js". You'll learn how to write testable, flexible, and maintainable code using principles (like this one) that I think all professional people in software should know about. Check it out.

Remember when I said that we wanted to be able to run tests on the UserController without having to pass in a UserRepo, solely because it would make the tests slow(UserRepo needs a db connection to run)?

Well, now we can write a MockUserRepo which implements IUserRepo and all the methods on the interface, and instead of using a class that relies on a slow db connection, use a class that contains an internal array of User[]s (much quicker! ⚡).

That's what we'll pass that into the UserController instead.

Using a MockUserRepo to mock out our UserController

repos/mocks/mockUserRepo.ts
import { IUserRepo } from '../repos';

class MockUserRepo implements IUserRepo {
  private users: User[] = [];

  constructor () {}

  async getUsers (): Promise<User[]> { 
    return this.users;
  }
}

Tip: Adding "async" to a method auto-wraps it in a Promise, making it easy to fake asynchronous activity.

We can write a test using a testing framework like Jest.

controllers/userRepo.spec.ts
import { MockUserRepo } from '../repos/mock/mockUserRepo';

let userController: UserController;

const mockResponse = () => {
  const res = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  return res;
};

beforeEach(() => {
  userController = new UserController(
    new MockUserRepo() // Speedy! And valid since it inherits IUserRepo.
  )
});

test ("Should 200 with an empty array of users", async () => {
  let res = mockResponse();
  await userController.handleGetUsers(null, res);
  expect(res.status).toHaveBeenCalledWith(200);
  expect(res.json).toHaveBeenCalledWith({ users: [] });
})

Congrats. You (more or less) just learned how write testable code!.

Inversion of Control & IoC Containers

Applications get much larger than just two components.

Not only do we need to ensure we're referring to interfaces and NOT concrete implementations, but we also need to handle the process of manually injecting instances of dependencies at runtime.

If your app is relatively small or you've got a style guide for hooking up dependencies on your team, you could do this manually.

If you've got a huge app and you don't have a plan for how you'll accomplish dependency injection within in your app, it has potential to get out of hand.

It's for that reason that Inversion of Control (IoC) Containers exist.

They work by requiring you to:

  1. Create a container (that will hold all of your app dependencies)
  2. Make that dependency known to the container (specify that it is injectable)
  3. Resolve the depdendencies that you need by asking the container to inject them

Some of the more popular ones for JavaScript/TypeScript are Awilix and InversifyJS.

Personally, I'm not a huge fan of them and the additional infrastructure-specific framework logic that they scatter all across my codebase.

If you're like me and you're not into container life, I have my own style guide for injecting dependencies that I talk about in solidbook.io. I'm also working on some video content, so stay tuned!

Inversion of Control: Traditional control flow for a program is when the program only does what we tell it to do (today). Inversion of control flow happens when we develop frameworks or enable a plugin architecture with areas of code that can be hooked into. In these areas, we might not know (today) how we want it to be used, or we wish to enable developers to add additional functionality. That means that every lifecycle hook in React.js or Angular is a good example of Inversion of Control in practice. IoC is also often explained by the "Hollywood Design Principle": Don't call us, we'll call you.



Discussion

Liked this? Sing it loud and proud 👨‍🎤.


0 Comments

Be the first to leave a comment

Submit

Stay in touch!



About the author

Khalil Stemmler

Khalil Stemmler is a Developer / Designer and co-founder of Univjobs. He frequently publishes articles about Domain-Driven Design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in Software Design



You may also enjoy...

A few more related articles

Why I Don't Use a DI Container | Node.js w/ TypeScript
Sep 16th, 2019 / 11 min read
Instead of a DI Container, I just package features by component and use logical naming conventions.
The 6 Most Common Types of Logic in Large Applications [with Examples]
Sep 16th, 2019 / 12 min read
In this article, you'll learn about the Clean Architecture, why we should separate the concerns of large applications into layers,...
Command Query Segregation | Object-Oriented Design Principles w/ TypeScript
Aug 29th, 2019 / 11 min read
CQS (Command-Query Segregation) is a design principle that states that a method is either a COMMAND that performs an action OR a Q...
Name, Construct & Structure | Organizing Readable Code - Part 1
Jun 15th, 2019 / 6 min read
Naming files & folders well, using well-understood technical constructs and strategically organizing files well are three ways to ...

Want to be notified when new content comes out?

Join 2000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates