This site lists the protips that we shared with students during our courses
One pattern that we see again and again in our code is called Dependency injection. Although it sounds and looks a bit daunting at first, it’s actually not that hard to understand and use.
I think it’s easiest to start with an example. Here’s a method that peforms a calculation of the saldo of all the accounts of a user
function calculateSaldo(userName, getUser, getAccounts) {
const user = getUser(userName);
const accountsForUser = getAccounts(user.id);
let saldo = accountsForUser.reduce((sum, account) => sum += account.balance, 0);
// ... and thousands of lines more that makes tricky calculcations
return saldo
}
In order to calculate this saldo we:
const user = getUser(userName)
)const accountsForUser = getAccounts(user.id);
)The real business functionality is that last part, summarizing the balance on the accounts. However, in order to do this we depend on some other parts to supply us with the data for the calculation. The two functions are dependencies to us.
Now, since I wrote the code to get the user and accounts I happen to know that it’s very simple. In fact, we could have written it rigth in the calculateSaldo
function if we wanted. Like this:
function calculateSaldo(userName) {
const user = userDb.find(user => user.name === userName);
const accountsForUser = accountDb.filter(account => account.userid === user.id);
let saldo = accountsForUser.reduce((sum, account) => sum += account.balance, 0);
// ... and thousands of lines more that makes tricky calculcations
return saldo
}
There are a couple of problems with this code:
calculateSaldo
without having it do exactly what it does now. That makes testing the calculateSaldo
much harder.For these, and probably more, reasons we can inject the dependencies / things that calculateSaldo
needs, in order for calculateSaldo
do it’s job.
Hence we can inject two parameters, the two functions that calculateSaldo
needs, into the function. Which brings us back to the first example, like this:
function calculateSaldo(userName, getUser, getAccounts) {
const user = getUser(userName);
const accountsForUser = getAccounts(user.id);
let saldo = accountsForUser.reduce((sum, account) => sum += account.balance, 0);
return saldo
}
The example above is known as a function injection, where we inject the dependencies into a single function. A special, but common, way of that is what’s known as constructor injection.
Let’s mimic, in part, what we had in the PlayerService
by creating a CalculatorService
class. It has a constructor like this:
function CalculatorService(userGetter, accountGetter){
this.getUser = userGetter;
this.getAccounts = accountGetter;
}
This constructor takes the two dependencies of the class as inparameters - we are constructor injecting the dependencies of CalculatorService
.
When we are creating a new class we get an object (instance of the object) back. We are storing the dependencies on the instance by using this
, this.getUser = userGetter
, for example.
We can then use these dependencies by using this
and the function we stored: const accountsForUser = this.getAccounts(user.id);
for example.
Last part, in order to create this class we need to do new CalculatorService
, like we are doing in the convince-method create
that we are exposing outside the CalculatorService
module.exports.create =
(userGetter, accountGetter) =>
new CalculatorService(userGetter, accountGetter);
Notice the use of new
that is passing the parameters to the create function into the constructor.
Also notice that the CalculatorService
knows (and cares) nothing about the dependencies we are passing to the constructor …
Almost. Here is how we are using it:
CalculatorService.prototype.calculateSaldo = function(userName) {
const user = this.getUser(userName);
const accountsForUser = this.getAccounts(user.id);
let saldo = accountsForUser.reduce((sum, account) => sum += account.balance, 0);
return saldo
}
See how we are using this.getUser
to get hold of the method we stored in the instance variable, in the constructor.
So CalculatorService
knows (and cares) nothing about the dependencies we are passing to the constructor… except that once we use it we:
this.getUser
to take a username and return the userthis.getAccounts
to take a id parameter and return the accoutns for the user with that id.How this is accomplished and implemented the CalculatorService
couldn’t care less about. Separation of concerns in actions.
Now if we want to use this we can do something like this, stiching all the parts together:
const userGetter = require('./dependencies').getUserFromDb;
const accountGetter = require('./dependencies').getAccountsForUser;
const calc = require('./CalculatorService')
.create(userGetter, accountGetter);
const marcusSaldo = calc.calculateSaldo('Marcus')
const jakobSaldo = calc.calculateSaldo('Jakob')
console.log(`Marcus has ${marcusSaldo}`);
console.log(`Jakob has ${jakobSaldo}`);
We use the .create
-function on the CalculatorService
and pass it the getUserFromDb
and getAccountsForUser
functions as dependencies for the class.
When we call the .calculateSaldo
function it will use the dependencies that is injected into the constructor.
I hope this made dependencies a bit more clear for you. I love to walk you through this if you want to.