Dependency injection: stop creating collaborators inside
Run the same class with different collaborators without editing the class itself.
Watch the narrated video
Every step
Problem
Steps 1–5Refactor
Steps 6–10Payoff
Steps 11–12Recap
Steps 13–14The code: before and after
Before
class RateCard {
constructor(private readonly perHour: number) {}
price(hours: number): number {
return hours * this.perHour;
}
}
class ReceiptPrinter {
constructor(private readonly branch: string) {}
print(bikeId: number, euros: number): string {
return `${this.branch}: bike ${bikeId}, ${euros} EUR`;
}
}
class ReturnDesk {
private readonly rates: RateCard;
private readonly printer: ReceiptPrinter;
constructor() {
this.rates = new RateCard(3);
this.printer = new ReceiptPrinter("City Center");
}
checkIn(bikeId: number, hours: number): string {
const euros = this.rates.price(hours);
return this.printer.print(bikeId, euros);
}
}
function rentals(): string[] {
const desk = new ReturnDesk();
return [desk.checkIn(7, 3)];
}After
class RateCard {
constructor(private readonly perHour: number) {}
price(hours: number): number {
return hours * this.perHour;
}
}
class ReceiptPrinter {
constructor(private readonly branch: string) {}
print(bikeId: number, euros: number): string {
return `${this.branch}: bike ${bikeId}, ${euros} EUR`;
}
}
class ReturnDesk {
constructor(
private readonly rates: RateCard,
private readonly printer: ReceiptPrinter,
) {}
checkIn(bikeId: number, hours: number): string {
const euros = this.rates.price(hours);
return this.printer.print(bikeId, euros);
}
}
function openDesk(perHour: number, branch: string) {
const rates = new RateCard(perHour);
const printer = new ReceiptPrinter(branch);
return new ReturnDesk(rates, printer);
}
function rentals(): string[] {
const city = openDesk(3, "City Center");
const airport = openDesk(5, "Airport");
return [city.checkIn(7, 3), airport.checkIn(7, 3)];
}Transcript, step by step
Problem
Step 1: A desk that builds its collaborators Before
This is a bike rental return desk. Rate Card holds the euros per hour and multiplies them by the hours. Receipt Printer holds a branch name and returns a line with the branch, the bike and the euros. Return Desk creates both in its own constructor, with three euros and City Center written right there. Check in asks the rate card for the price and tells the printer to print it. At the bottom, the rentals function checks in bike seven. It was out for three hours. So the receipt reads City Center, bike seven, nine euros. With one branch, a desk that builds its own rate card and printer was the shortest path, and callers knew nothing.
Step 2: An airport branch Change request
Now a second branch opens at the airport, and it charges five euros an hour and prints Airport on its receipts. Both desks run in the same program, so the quick way gives Return Desk's constructor a branch name. Inside, an if builds the airport rate card and printer, and the else keeps the city ones. tsc is happy, and the rentals function checks in bike seven at each desk. City Center's receipt says nine euros. The Airport's receipt says fifteen.
Step 3: The desk picks its own collaborators Code smell
Return Desk calls the rate card and the printer to do part of its job, so each one is a collaborator. Return Desk holds the check-in rule, yet it also picks its rate card and printer, because it creates its collaborators inside. It picks the branch here, builds the airport collaborators here, and builds the city collaborators here. Check in didn't change, yet Return Desk was edited for a change that's only about prices and names. And a third branch means one more else if, in the same constructor.
Step 4: The else hides a typo Code smell
Worse, here the rentals function passes Airport with a capital A. What does that desk charge? Three euros an hour, the city rate, because this comparison is case-sensitive and the else catches it. So the airport desk returns a City Center receipt for nine euros.
Step 5: Dependency injection Principle
The desk should get its rate card and printer from whoever builds it. A class that receives its collaborators from outside, instead of creating them, uses dependency injection. So the rentals function would hand the desk its rate card and printer, and the desk would only use them.
Refactor
Step 6: Refactor first Back it out
So we take the branch parameter and the if back out, and the desk serves City Center alone again. We refactor first in small steps with the same receipt each time, and the airport comes back at the end.
Step 7: Pass the rate card in Change Function Declaration
Return Desk's constructor gets a Rate Card parameter and stores it, so it no longer creates one. Because the constructor's signature changes, the move is called Change Function Declaration. When a constructor receives a collaborator this way, it's called constructor injection. The line that builds the rate card with three moved into the rentals function, which passes it in, so the receipt stays.
Step 8: Pass the printer in Change Function Declaration
The same constructor injection again for the printer, so the constructor takes a Receipt Printer too. Private readonly in front of a parameter makes it a field as well, and TypeScript calls that a parameter property. So the two field lines and both assignments are gone, the same way Rate Card already stores its rate. Now the constructor only stores what it's given, so it names no rate and no branch. The rentals function builds the printer with City Center and passes both collaborators in, and the receipt hasn't changed.
Step 9: One function opens a desk Extract Function
The airport would need a copy of those three build lines, so we extract them into a function, open desk. It takes the rate and the branch name, builds both collaborators from them, and returns a new desk. A function whose only job is to build and return an object is called a factory function. The rentals function calls open desk with three and City Center, and the receipt is still the same. The cost is one more function to read, and each new setting becomes one more parameter here.
Step 10: One place wires the collaborators Composition root
Something still has to create the objects and wire them, so open desk and the rentals function do it. The code that builds and wires every object, open desk and the rentals function, is called the composition root. It's the only code that knows the rate and the branch name. Return Desk, meanwhile, keeps the check-in rule and nothing else.
Payoff
Step 11: The airport, take two Payoff
So the airport request from the start can come back in now. The rentals function calls open desk with five and Airport, so a second desk opens beside the city one. City Center's receipt still says nine euros. The Airport's receipt still says fifteen, the same as the quick version.
Step 12: The desk did not change Payoff
Return Desk didn't change at all to add the airport. The constructor only stores its collaborators now. Both branches' collaborators are built here, from the arguments. Return Desk keeps the check-in rule and the caller chooses the collaborators, so a new branch is one more call. And a misspelled branch name can't fall into the wrong prices, because there's no name left to compare. You might think dependency injection needs an interface. It doesn't, because both collaborators stay concrete classes here, and only their creation moved. But now the composition root must know every collaborator, so each new branch is one more open desk call by hand.
Recap
Step 13: Where we started, where we ended Before / after
On the left, Return Desk created its rate card and printer itself, with three and City Center written in its constructor. So a second branch meant editing the desk, even though its check-in rule stayed the same. On the right, the desk receives both collaborators as parameter properties and only stores them. And open desk, in the composition root, builds each branch's collaborators from its arguments.
Step 14: When to reach for it Takeaways
Reach for it when one class must run with different collaborators in the same program, like two branches or two currencies. Skip it when a class has one setup that no caller will ever change. Skip it too when the collaborator is a plain array it fills, because passing it in only moves the creation outward. Spot it by a constructor that builds configured objects with values written inside, or takes a flag to pick them. And Node's Console class works this way, because it takes the streams it writes to as constructor arguments.