Clean Code Basics: DI - Dependency Injection
Let’s talk about a very important topic, in the software engineering world, which is Dependency Injection (DI)
Welcome back, brave coder! In today’s article, we’ll be covering a very important topic, in the software engineering world, which is Dependency Injection (DI). As always, we are here to have fun and learn at the same time, so in today’s article you’ll learn about Dependency Injection (DI) while helping a wizard 🧙 and a dragon 🐉 work together haha. I promise you, it’s going to be fun, simple, and beginner-friendly!
Without wasting time, let’s get started, Ala Barakati Lah!
Level 1: Understanding the Quest
Imagine you’re a game developer. You want to create a story where a wizard can cast spells, and a dragon can breathe fire. To do that, the wizard needs a spellbook 📖, and the dragon needs fire breath 🔥.
Here’s how you might start:
class Spellbook {
castSpell() {
console.log("Zap! The wizard casts a spell. ✨");
}
}
class Dragon {
breatheFire() {
console.log("Roar! The dragon breathes fire. 🔥");
}
}
class Wizard {
constructor() {
this.spellbook = new Spellbook(); // ❌Oh noo! Direct dependency!
}
attack() {
this.spellbook.castSpell();
}
}It looks fine, right? But there’s a problem, actually! The Wizard is directly creating a Spellbook, meaning the two (Wizard & Spellbook) are tightly connected❌(tight coupling).
What happens if you want the Wizard to use a different spellbook? → 😱Can’t!
Or no spellbook at all? → 😱Forced to use a Spellbook!
Result: This tight dependency makes things forced.
Level 2: Discovering Dependency Injection
To make your code more flexible, you can use Dependency Injection (DI). DI means giving a class (like the Wizard) everything it needs (dependencies) instead of letting it create those dependencies itself. In our case, we’ll give the Wizard the Spellbook.
Here’s the refactored code:
class Spellbook {
castSpell() {
console.log("Zap! The wizard casts a spell. ✨");
}
}
class Wizard {
constructor(spellbook) { // ✅Injecting the dependency!
this.spellbook = spellbook;
}
attack() {
this.spellbook.castSpell();
}
}
}
// Now, you can inject the spellbook:
const mySpellbook = new Spellbook();
const myWizard = new Wizard(mySpellbook); ✅
myWizard.attack();
// Output: Zap! The wizard casts a spell. ✨See what we did there? The Wizard doesn’t care where the spellbook came from. All it knows is how to use it. This is the magic of Dependency Injection!
Level 3: The DI list of Benefits
Why should you use Dependency Injection? Here's what you gain:
Flexibility: You can swap out dependencies. What if the wizard gets a new spellbook or the dragon switches to ice breath? No problem!
Testability: You can replace real dependencies with fake ones for testing.
Reusability: Your classes (Wizard and Dragon) are not tied to specific implementations.
Let’s demonstrate swapping dependencies for the Wizard:
class FancySpellbook {
castSpell() {
console.log("Boom! The wizard casts a powerful spell. 💥");
}
}
const fancySpellbook = new FancySpellbook();
const upgradedWizard = new Wizard(fancySpellbook);
upgradedWizard.attack();
// Output: Boom! The wizard casts a powerful spell. 💥You see that we simply passed, to the Wizard, a new kind of spellbook named FancySpellbook, the wizard didn’t care at all, and just called the attack property!
PS: We need to pass to the wizard instance with the same base structure (contains a method called castSpell(), else an error will ocure! You can use a interface to define your base type.
Level 5: Dependency Injection Frameworks (optional)
In big projects, managing dependencies manually can get messy. That’s where tools like InversifyJS for example come in! These frameworks automate Dependency Injection for you.
PS: I’m just giving here an example using a js library, if you are using NestJS for example, the DI Pattern is already built-in ready to use. Or if you’re a Java Spring Boot user, the
@Autowiredannotation is your way to go.
Here’s how we can use DI in our last example, using InversifyJS:
const { Container, injectable, inject } = require("inversify");
@injectable()
class Spellbook {
castSpell() {
console.log("Zap! The wizard casts a spell. ✨");
}
}
@injectable()
class Wizard {
constructor(@inject(Spellbook) spellbook) {
this.spellbook = spellbook;
}
attack() {
this.spellbook.castSpell();
}
}
const container = new Container();
container.bind(Spellbook).toSelf();
container.bind(Wizard).toSelf();
const myWizard = container.get(Wizard);
myWizard.attack();
// Output: Zap! The wizard casts a spell. ✨Very simple right? You simply define your classes and add the corresponding annotations, bind classes to the container and that’s it! Whenever you want an instance, the container will ship it to you ready to use!
Congratulations 🏆
Congratulations! You’ve mastered the basics of Dependency Injection. Now your code is more flexible, testable, and ready for advanced use cases!
That’s it for today’s tutorial, I hope you found it useful, see you in the next one, bye!
🔔 Don't forget to subscribe to my blog and YouTube channel to stay updated with all the latest content!



