Photo by Mpho Mojapelo on Unsplash
originally published on bostonc.dev
Ever since my first job as a developer, people I've worked with talked about dependency injection and how essential it was. Even with professional programming experience under my belt, it was difficult for me to understand what it really was. Online resources would use more jargon that only left me more confused.
This post will hopefully provide a simple explanation for those new to software development and architecture on what dependency injection is and why it is needed.
Let's say you have a car. For you I imagine you love to play music in your car (and especially love to sing along if you are like me 🎤 🎶).
You got a nice deal on your car. There is a problem though, the stereo in your car is special. It can only play music from a CD which is welded into the stereo. This means that if you get tired of singing Kool & The Gang, you will have to go and get a whole new stereo and then replace your current stereo with it.
class Stereo () {
constructor(cdName, speakers) {
this.cdName = cdName;
this.speakers = speakers;
}
function play() {
speakers.playMusic(this.cdName);
}
}
This is a lot of manual work and will take some time. Fortunately, stereos aren't actually built this way. Instead, they have a CD reader, in which you can put many CDs in it. It can read any CD as long as the stereo and the CD speak the same language (you can't put a BluRay disk in your stereo!). You can hold CDs and change it any time you want. This is dependency injection.
class Stereo () {
constructor(speakers) {
this.speakers = speakers;
}
function play(cdName) {
speakers.playMusic(cdName);
}
}
This may seem like an obvious way to go about it since we have been using CDs and stereos a long time (although going away with everything being streamed!). However, software is typically written tightly coupled as described in the scenario. Its normal to have an application getting data from an outside source such as a database. But what happens when the client wants it to connect to a different database? With dependency injection, it's as simple as switching them out like a CD in a stereo.
Let's imagine we are writing an application to display current movies shown in theaters to a user. Take a look a this pseudocode:
// function to display movies to the user
function displayMovies() {
// get movies
var movies = getMovies()
// display movies
showMovies(movies)
}
// function to get movies from the database
function getMovies() {
// connect to our DB (currently a SQLDB)
var db = new SQLDB(url).Connect()
// query for movies
var movies = db.ExecuteQuery('SELECT * FROM movies')
return movies;
}
Above we have a simple function in order to get the movies data from a SQL database by connecting to it and executing a query, then finally displaying it to the user.
This works, but is hard to test! In order to test it, we need to have a SQLDB running on any machine we test on populated with mock data. As well as in the future, if suddenly we needed to connect to a different database, such as a NOSQL database, then we have a lot of work to change.
If we wanted to be able to test this easily or change databases, we need to loose the tight coupling from the application and the database. We can do this by defining an interface for a DB with a set of methods it can call (the language spoken) and then ensure that any database we use fulfils that interface.
Let's look at how this could be done (again, psuedocode):
// movie database interface
interface MovieDB {
GetAllMovies()
GetMovie(name)
// .. other methods needed to manipulate movie database
}
// class for MovieDB held on SQLDB
class SQLMovieDB implements MovieDB {
// variable to hold db object
var db;
// constructor for when SQLMovieDB is created
constructor(url) {
// connect to DB
db = new SQLDB(url).Connect()
}
// get all movies
GetAllMovies() {
var movies = db.ExecuteQuery('SELECT * FROM movies')
return movies
}
// get a specific movie
GetMovie(name) {
var movie = db.ExecuteQuery('SELECT * FROM movies where movies.name is ' + name)
return movie
}
// ... other methods to implement interface
}
Nice! Now we have abstracted the movie database into an interface that can be implemented by our SQLMovieDB. Now to display the movies, we can do as so:
Note: For languages that don't support interfaces, you can pass additional parameters to functions to replicate the same behavior.
// function to display movies to the user
function displayMovies() {
// get movies
var movies = getMovies()
// display movies
showMovies(movies)
}
// function to get movies from the database
function getMovies() {
// connect to our DB
var db = new SQLMovieDB(url)
// query for movies
var movies = db.GetAllMovies()
return movies;
}
It is even easier to test! We can create a mock movie DB to be used in our tests:
class MockMovieDB implements MovieDB {
constructor(url) {
// don't need to do anything
}
// get all movies
GetAllMovies() {
var movies = [{ title: 'SOME SUPER COOL MOVIE', ...otheData }, ...otherMovies]
return movies
}
// get a specific movie
GetMovie(name) {
var movie = { title: 'SOME SUPER COOL MOVIE', ...otherData }
return movie
}
// ... other methods to implement interface
}
Now we can ensure for the tests that we use the MockMovieDB and easily mock the data without actually having to stand up a database server on the test machine.
Finally, it makes it easy to use another database in the future if needed, for example, using a NOSQL database:
// class for MovieDB held on NOSQLDB
class NOSQLMovieDB implements MovieDB {
var db;
constructor(url) {
db = new NOSQLDB(url).Connect()
}
// get all movies
GetAllMovies() {
var movies = db.collection('movies').find({});
return movies
}
// get a specific movie
GetMovie(name) {
var movie = db.collection('movies').find({ title: name });
return movie
}
// ... other methods to implement interface
}
Now to use this NOSQLMovieDB we only have to change one line in our displayMovies
function:
// function to display movies to the user
function displayMovies() {
// get movies
var movies = getMovies()
// display movies
showMovies(movies)
}
// function to get movies from the database
function getMovies() {
// connect to our DB
var db = new NOSQLMovieDB(url) // <---- WE ONLY HAVE TO CHANGE HERE
// query for movies
var movies = db.GetAllMovies()
return movies;
}
We could even move this to an environment variable if we so wanted. We can even eliminate the need for ever rewriting the getMovies
function by injecting a MovieDB implementor as a parameter!
Once I really understood dependency injection, a whole world of great software design patterns opened up to me. By using these patterns my code has become cleaner and safer.
Let me know what you think!
Special thanks to Adam Whitehurst for helping prepare this post. 🤘
Top comments (0)