I'll Be Backend
Design patterns · Java

Dependency injection: stop creating collaborators inside

Run the same class with different collaborators without editing the class itself.

14 steps4 chapters~6 min at 1.25×#dependency injection#constructor injection#composition root#decoupling
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the Java video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–5

Refactor

Steps 6–10

Payoff

Steps 11–12

Recap

Steps 13–14

The code: before and after

Before

record RateCard(int perHour) {
    int price(int hours) { return hours * perHour; }
}

record ReceiptPrinter(String branch) {
    void print(int bikeId, int euros) {
        System.out.println(branch + ": bike " + bikeId
            + ", " + euros + " EUR");
    }
}

class ReturnDesk {
    private final RateCard rates;
    private final ReceiptPrinter printer;

    ReturnDesk() {
        rates = new RateCard(3);
        printer = new ReceiptPrinter("City Center");
    }

    void checkIn(int bikeId, int hours) {
        printer.print(bikeId, rates.price(hours));
    }
}

class Rentals {
    public static void main(String[] args) {
        new ReturnDesk().checkIn(7, 3);
    }
}

After

record RateCard(int perHour) {
    int price(int hours) { return hours * perHour; }
}

record ReceiptPrinter(String branch) {
    void print(int bikeId, int euros) {
        System.out.println(branch + ": bike " + bikeId
            + ", " + euros + " EUR");
    }
}

class ReturnDesk {
    private final RateCard rates;
    private final ReceiptPrinter printer;

    ReturnDesk(RateCard rates, ReceiptPrinter printer) {
        this.rates = rates;
        this.printer = printer;
    }

    void checkIn(int bikeId, int hours) {
        printer.print(bikeId, rates.price(hours));
    }
}

class Rentals {
    static ReturnDesk openDesk(int perHour, String branch) {
        RateCard rates = new RateCard(perHour);
        ReceiptPrinter printer = new ReceiptPrinter(branch);
        return new ReturnDesk(rates, printer);
    }

    public static void main(String[] args) {
        openDesk(3, "City Center").checkIn(7, 3);
        openDesk(5, "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, and Rate Card holds the euros per hour and multiplies them by the hours. Receipt Printer holds a branch name and prints 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. Main checks in bike seven after three hours, and the receipt says 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 with five and Airport, and the else keeps three and City Center. It compiles, and main checks in bike seven at each desk, so it prints nine euros, then 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 main passes Airport with a capital A. What does that desk charge? Three euros an hour, the city rate, because this equals check fails and the else catches it.

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. In plain words, whoever builds the desk hands it the rate card and the printer, and the desk only uses 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. This move is Change Function Declaration, and when a constructor gains a collaborator this way, it's called constructor injection. Through the constructor, the rate card field stays final, so no desk can exist without one. The line that built the rate card with three moved into main, which passes it in, and the receipt is the same.

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. Now it only stores what it's given, so it names no rate and no branch. Main builds the printer with City Center and passes both collaborators in, and the receipt hasn't changed.

Step 9: One method opens a desk Extract Function

The airport would need a copy of main's three build lines, so we extract them into a static method, open desk. It takes the rate and the branch name, builds both collaborators from them, and returns a new desk. A static method whose job is to build and return an object is called a static factory method. Main calls open desk with three and City Center, and the receipt is still the same. Each new setting, such as a currency, adds one more parameter to open desk.

Step 10: One place wires the collaborators Composition root

Main and open desk are now the one place near the program's start that creates the objects and wires them together. The one place that builds and wires every object 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. Main calls open desk with five and Airport, and checks in bike seven for three hours. It prints nine euros at City Center and fifteen at Airport, the same two receipts as the quick version.

Step 12: The desk did not change Payoff

Return Desk didn't change at all to add the airport. Where the if stood, the constructor only stores its collaborators, and 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 injection needs an interface. It doesn't, because both collaborators stay concrete classes here, and only their creation moved. But now the composition root has to know every collaborator, and it builds each desk 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 through its constructor 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 list 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 Java's own Print Writer works this way, because it takes the writer it prints to through its constructor.