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 thegetBanana
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.