Illustrating Dependency Injection Using Shakespeare and a Quill Pen
This article assumes that you’re a beginner with a basic understanding of Object-Oriented Programming, and that you can relate with the idea of a writer named “Shakespeare” who depends on pen and paper to write.
Think of real life objects: a writer named “Shakespeare" who needs pen and paper to write. Shakespeare is a writer and there are many other writers who may write in ways slightly different from how Shakespeare would. Nonetheless, each writer must have basic writing abilities before they can be considered a writer. These abilities include “create a title" and “describe incidents, objects or people". A writer who lacks any of these abilities cannot be considered a writer. Shakespeare is already a renowned writer and we know he has these abilities. However, when a new writer wants to come onboard, there’s a checklist of required abilities and properties without which such a writer cannot just work. In Object-oriented Programming (OOP), especially for the sake of this discourse, the checklist of required abilities is called an Interface. Let’s use code to create an interface for writers:
Interface IWriter {
pen: IPen;
createTitle();
describeIncidentsObjectsOrPeople();
}Interface IPen {
write(text: string);
}
Why put an “I” before “Writer”? By convention, “I” helps to identify an interface easily so that wherever we see “I” followed by a sequence of alphabets, we know we’re dealing with an interface not an object.
Now that there’s an interface that defines what functions a writer should be able to perform, we can create a Shakespeare object that implements IWriter interface.
class Shakespeare implements IWriter {
pen: IPen; constructor(pen: QuillPen) {
this.pen = pen;
} public createTitle() {
this.pen.write("Romeo and Juliet");
} public describeIncidentsObjectsOrPeople() {
this.pen.write("A calm garden full of chirping crickets");
}}
Shakespeare is now considered a type of writer because Shakespeare implements all the functions required of a writer as defined in IWriter interface. In addition, we have limited Shakespeare to use only a quill pen by defining it in Shakespeare’s constructor. This means that Shakespeare may write with only a quill pen — no fountain pen, roller-tip or ballpoint pen.
As a tool that Shakespeare depends on to function, we need to define and create a quill pen object, then inject it as a dependency to Shakespeare. We could define a quill pen object like this:
class QuillPen implements IPen {
public write(text: string) {
console.log(text);
}
}
The quill pen object has implemented the write() function required in IPen. Now we can create a quill pen object and pass it to Shakespeare via dependency injection (DI) so that he can write with it.
Injecting a Dependency
Dependency Injection is simply “giving Object A to an Object B because Object B needs Object A to function”. These objects can be passed as constructor arguments or function arguments. It is similar to giving Shakespeare a pen to write with.
const quillPen = new QuillPen();
const shakespeare = new Shakespeare(quillPen);
What we’ve done is a simple dependency injection (DI) — we used DI without a DI container. You have done a simple dependency injection when you hard-code the injection of an instance of class B into a class A. In the code example above, we created a new instance of QuillPen and sent it as a dependency to Shakespeare via the constructor. In simple DI, if we want Shakespeare to use a fountain pen instead of a quill pen, we have to create a fountain pen object and inject it to Shakespeare. However, this requires updating the Shakespeare class to allow other types of pen because we previously tightly coupled Shakespeare to a quill pen.
class Shakespeare implements IWriter {
pen: IPen; constructor(pen: ̶Q̶u̶i̶l̶l̶P̶e̶n̶ IPen) {
this.pen = pen;
} ...
}
Henceforth, Shakespeare is free to use any type of pen because we’ve updated the constructor to allow not just a quill pen but any pen that implements IPen interface.
The act of injecting an interface (abstract type) instead of an object (concrete type) is called “Dependency Inversion” or “Inversion of Control (IOC)”. You can read more on Dependency Inversion later.
Now, we can create a fountain pen that implements IPen and inject it to Shakespeare without issues.
class FountainPen implements IPen {
write(text: string) {
console.log(text);
}
}const fountainPen = new FountainPen();
const quillPen = new QuillPen();const shakespeareUsingFountainPen = new Shakespeare(fountainPen);
const shakespeareUsingQuillPen = new Shakespeare(quillPen);
This leads us to an important concept in Dependency Injection — the Dependency Injection Container (DI container).
The DI Container
Vacancy! A pen collection manager is needed to help Shakespeare manage his large collection of pen. Your role is to deduce what pen Shakespeare needs per time and hand it over to him. If you think it’s a honour to work with one of the greatest writers in history, please apply.
Why would Shakespeare recruit a pen collection manager? It probably makes sense when he has a very large collection of pen. It’s also fun to have someone say “Hey Shakespeare! My job description tells me to give you a fountain pen here. So here’s a fountain pen.” Think of the pen collection manager as a parody for the DI Container whose function is to register, create and inject objects as a dependency to other objects. Technically, a DI Container for Shakespeare would refer to a class or set of classes that collects different types of pen objects (fountain pen, quill pen, roller-tip pen etc) and injects them to Shakespeare when needed.
A DI container can be implemented in different ways. However, its primary responsibility is to be the glue that binds abstractions (i.e. an injected interface e.g. IPen) to concrete types (i.e. objects that implement these interfaces, having the functions and properties defined in the interface e.g. FountainPen and QuillPen). In the previous example, we created two different Shakespeare objects (shakespeareUsingFountainPen and shakespeareUsingQuillPen) based on the type of pen injected to them. What if we want just one Shakespeare object that is able to use any type of pen at runtime so that we don’t have many Shakespeares flying around? We have taken the first step of injecting an interface (IPen) to Shakespeare’s constructor instead of a concrete object (QuillPen). This makes Shakespeare more scalable because it can use any type of pen. With the DI Container, the type of pen that Shakespeare needs is automatically detected and provided to Shakespeare at runtime. DI container gives you features such as autowiring.
“Autowiring is an exotic word that represents something very simple: the ability of the [Dependency Injection] container to automatically create and inject dependencies.”
— PHP-DI
Closing Remarks
It’s important to note that you don’t have to use a DI container to be doing Dependency Injection.
“A DI Container is an optional library that can make it easier for us to compose components when we wire up an application, but it’s in no way required. When we compose applications without a DI Container [,] we call it Pure DI…”
Source: Manning
Dependency Injection may not require a DI container at all. If you’re working on a small application with no complex injection happening, you might be tempted to write a simple DI container yourself. It’s a plus to be able to write a DI container; however, it is not a good idea to write your own DI container. You should use simple dependency injection for small applications and upscale to an established DI library when the application grows to a point where maintaining dependency injections is no longer straightforward.
Examples of DI container libraries with good documentation that you can upscale to include:
- PHP-DI (for the PHP programming language)
- Inversify, TypeDI, Injection-js (for JavaScript and TypeScript)
If you built your application on frameworks such as Laravel, Spring or Angular, they come with DI containers baked-in.