Wtf is dependency injection? (JavaScript edition)

Wtf is dependency injection? (JavaScript edition)

You might be using it already.

Introduction

Dependency injection.

You might be using it already.

It’s straightforward and a nice way to give more control to the consumers of the functions or classes.

Let’s take a look at an example. I’ll use both classes and functions in JavaScript.

The problematic path

Let’s say we’ve a function that always does smoothies.

So far, it’s always done them using bananas and strawberries.

// Using functions

function makeSmoothie() {
  let banana = getBanana();
  let strawberry = getStrawberry();
  // ...blend it all together...
  return `${banana} and ${strawberry} smoothie! Yum!`;
}

function getBanana() {
  // Imagine this function goes out to a Banana farm and gets a banana
  return 'banana';
}

function getStrawberry() {
  // This function, on the other hand, visits a berry patch.
  return 'strawberry';
}
// Using classes

class SmoothieMaker {
  constructor() {
    this.banana = this.getBanana();
    this.strawberry = this.getStrawberry();
  }

  getBanana() {
    // Gets banana from a specific source
    return 'banana';
  }

  getStrawberry() {
    // Gets strawberry from a specific source
    return 'strawberry';
  }

  makeSmoothie() {
    return `${this.banana} and ${this.strawberry} smoothie! Yum!`;
  }
}

const mySmoothieMaker = new SmoothieMaker();
console.log(mySmoothieMaker.makeSmoothie()); // Outputs: banana and strawberry smoothie! Yum!

The problems

The above examples may seem silly, but imagine we're working in a large codebase.

The functions and classes are larger. They're used in multiple places.

We have several issues here:

  • Hard to maintain: The code isn't flexible. Imagine we want to create smoothies using other ingredients. It's gonna be painful.

  • Hard to test: Let's imagine for a second getBanana function is an async function that fetches a specific type of banana. In our tests, it'd be easier to mock the getBanana function. But now we have no ability to do so.

Dependency injection to the rescue

Here is the code after dependency injection:

// Using functions

function makeSmoothie(fruit1, fruit2) {
  // ...blend it all together...
  return `${fruit1} and ${fruit2} smoothie! Yum!`;
}

const banana = getBanana();
const strawberry = getStrawberry();
console.log(makeSmoothie(banana, strawberry)); // Banana and strawberry smoothie! Yum!
// Using classes

// Define functions for fetching ingredients
function getMango() {
  return 'mango';
}

function getPineapple() {
  return 'pineapple';
}

// The SmoothieMaker now expects two functions as its dependencies
class SmoothieMaker {
  constructor(getFruit1, getFruit2) {
    this.getFruit1 = getFruit1;
    this.getFruit2 = getFruit2;
  }

  makeSmoothie() {
    let fruit1 = this.getFruit1();
    let fruit2 = this.getFruit2();
    return `${fruit1} and ${fruit2} smoothie! Yum!`;
  }
}

// When creating a new SmoothieMaker, we inject the functions directly
const mySmoothieMaker = new SmoothieMaker(getMango, getPineapple);
console.log(mySmoothieMaker.makeSmoothie()); // Outputs: mango and pineapple smoothie! Yum!

Now, the code is much more flexible.

This is easier to maintain, test and use.

Conclusion

Dependency injection may be a fancy word.

But we use this pattern all the time.