šŸš€ The Ultimate Guide to Dependency Injection Without Libraries!

Vijay Rangan
5 min readMay 29, 2023

Introduction

Greetings, fellow developers!
Today, we embark on a journey into the realm of dependency injection in JavaScript, with no external libraries.

Brace yourself for an immersive exploration of this powerful technique as we delve into creating Janeā€™s Coffee Shop. With a touch of pragmatism and a sprinkle of technical finesse, weā€™ll unravel the secrets of dependency injection, enabling you to craft flexible, scalable, and maintainable JavaScript applications.

Software, as opposed to hardware, is meant to be easily changeable. Hence the name.

Unveiling the Essence of Dependency Injection

Before we dive into the practical implementation, letā€™s understand the essence of dependency injection.

Imagine Janeā€™s Coffee Shop, a bustling hub for caffeine enthusiasts. Jane, a coffee connoisseur, realises that she canā€™t excel in every aspect of her business single-handedly. Instead, she gets help from a coffee vendor (Coffee Guru) that provides the beans and a barista (Barista Bob) to turn those beans into ā˜•ļø. This allows Jane to focus on what she does best ā€” running her business and serving delightful cups of coffee to her loyal customers ā€” by delegating the responsibility of buying the beans, roasting them and finally grinding them to make coffee.

Much like how Jane depends on others to help with her business, in software development, your code has different components, objects and modules that depend on each other to function correctly.

Setting the Stage

To kickstart our journey, we lay the groundwork for our application. We initialise a simple NodeJS project called coffee-shop with the following directory structure. The code for it is available at https://github.com/vjrngn/coffee-shop

To express Janeā€™s coffee shop in code, weā€™ll create a few classes. The CoffeeGuru class is our vendor. It provides all the raw materials required to make coffee.

class CoffeeGuru {
getCoffeeBeans() {
// Fetch the finest coffee beans
}

getWater() {
// Retrieve purified water
}

getMilk() {
// Procure fresh milk
}
}

The Barista class is responsible for actually making the coffee.

class Barista {
brewCoffee() {
// ... makes amazing coffee
}
}

Finally, we create a CoffeeShop class to bring everything together. Letā€™s actually see how they all fit together.

class CoffeeShop {
constructor(barista) {
this.barista = barista;
}

makeCappuccino() {
return this.barista.brewCoffee();
}
}

Harnessing the Power of Dependency Injection

Jane uses CoffeeGuru to procure all the materials she requires so letā€™s use an instance of CoffeeGuru by ā€œinjectingā€ it into the Barista .

class Barista {
constructor(coffeeGuru) {
this.ingredientProvider = coffeeGuru;
}

brewCoffee() {
// ...
}
}

In code, injection means constructing the class with an instance of CoffeeGuru . This is known as constructor injection. There are other forms of injection such as method injection and property injection but the easiest and most widely used is constructor injection.

As simple as that! Letā€™s prepare some hot, piping cappuccino!

Weā€™ll update our Barista so they can use all the wonderful beans and milk and other ingredients from CoffeeGuru

class Barista {
constructor(coffeeGuru) {
this.ingredientProvider = coffeeGuru;
}

brewCoffee() {
const coffeeBeans = this.ingredientProvider.getCoffeeBeans();
const water = this.ingredientProvider.getWater();
const milk = this.ingredientProvider.getMilk();

// Brew the perfect cup of cappuccino using the ingredients
// ...
}
}

Bringing it all together

Now that we have our vendor and barista all ready to go, we can start actually selling our coffee to customers.

We start by creating an instance CoffeeGuru . Next, we create an instance of Barista , injecting it with an instance of CoffeeGuru . Finally, we create our CoffeeShop and make it use (or inject) the Barista .

Bringing it all together in main.js

With the Coffee Guruā€™s expert guidance and the art of dependency injection, Jane can now serve her customers with the finest caffeine experience.

Phew! That was a LOT of work just to serve ā˜•ļø.

ā€œWhy go through all this hassle? šŸ¤”ā€, you may wonder. Good question!

To see why this technique is so powerful, letā€™s say Jane decides her vendor is too expensive and she can no longer buy from them. In this contrived example, this change is simple. However, it showcases a very common occurrence in software development where things change frequently. For example, we decide to use Stripe over RazorPay for accepting customer payments, use raw SQL instead of an ORM to perform database queries etc...

You will very soon realise, dealing with all this without DI becomes untenable. Software, as opposed to hardware, is meant to be easily changeable. Hence the name. DI is one of the techniques that allows software to beā€¦ ā€œsoftā€

To deal with a change in vendor, all we would need to do is create another class, say CoffeeCo , and use that instead of CoffeeGuru .

By doing so, we only needed to change the vendor and nothing about the coffee making process needed to change! Awesome right!?šŸ”„

Letā€™s do a quick refactor

The astute amongst you would have noticed I named the variable this.ingredientProvider in the Baristaclass instead of this.coffeeGuru .

class Barista {
constructor(coffeeGuru) {
this.ingredientProvider = coffeeGuru;
}

brewCoffee() {
const coffeeBeans = this.ingredientProvider.getCoffeeBeans();
const water = this.ingredientProvider.getWater();
const milk = this.ingredientProvider.getMilk();
// Brew the perfect cup of cappuccino using the ingredients
// ...
}
}

Since Barista doesnā€™t really care about which exact vendor provides the beans, naming our variable this way allows us to reference the provider in a more abstract form rather than using this.coffeeGuru as the vendor may change in the future. We can now update our Barista class like so:

class Barista {
constructor(ingredientProvider) {
this.ingredientProvider = ingredientProvider;
}

brewCoffee() {
const coffeeBeans = this.ingredientProvider.getCoffeeBeans();
const water = this.ingredientProvider.getWater();
const milk = this.ingredientProvider.getMilk();
// Brew the perfect cup of cappuccino using the ingredients
// ...
}
}

This change is minor and doesnā€™t affect the functionality of the Barista class. However, since code is read more than it is written, this enhances intent, as the dependency ingredientProvider clearly states that the Barista needs a vendor to provide ingredients but doesnā€™t care about a specific vendor such as CoffeeGuru .

If you would like to understand further about how dependency injection helps with maintaining and testing code, subscribe to get notified of my next post where I talk about exactly this concept.

If you liked this article and you learned something, donā€™t be shyā€¦ show some love with a šŸ‘ . It really helps me reach a wider audience and motivates me to write more useful articles. šŸ™ŒšŸ»

Until next timeā€¦

--

--