Low-level designCore9 min

Dependency inversion principle

The code that decides what to do shouldn't depend on the code that does the low-level work. Both depend on an interface the decision-making code owns, so details can change without touching the policy.

Watch it evolve1 / 10
The problem

A plan that waters a greenhouse

WateringPlan decides which zones need water and for how long. ValveBoard opens a numbered channel on the relay board.

80
Start
Stage 1
Stage 2
Final

1 · The situation

A watering plan that drives a valve board

A greenhouse waters its beds automatically. Each zone has a soil moisture reading, and WateringPlan decides which zones need water and for how long: the drier the soil below the target, the longer the valve stays open. That rule is the part the growers care about and change.

The valves hang off a relay board. ValveBoard knows how to drive it: pulse(channel, seconds) opens one numbered channel. The first version has the plan create a board and call it. The names are the same in all four languages; a zone is a small value with a name, a channel and a reading.

The first versionJava
import java.util.List;

record Zone(String name, int channel, int moisture) {}

class ValveBoard {
    void pulse(int channel, int seconds) {
        System.out.println("board: channel " + channel + " open for " + seconds + " s");
    }
}

class WateringPlan {
    private final ValveBoard board = new ValveBoard();
    private final int target;

    WateringPlan(int target) {
        this.target = target;
    }

    int water(List<Zone> zones) {
        int watered = 0;
        for (Zone zone : zones) {
            if (zone.moisture() >= target) continue;
            board.pulse(zone.channel(), 2 * (target - zone.moisture()));
            watered++;
        }
        return watered;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Zone> siteA = List.of(
                new Zone("Herbs", 1, 22),
                new Zone("Tomatoes", 2, 41),
                new Zone("Seedlings", 3, 30));
        WateringPlan plan = new WateringPlan(35);
        int watered = plan.water(siteA);
        System.out.println("Site A: watered " + watered + " of " + siteA.size() + " zones");
    }
}
1«record»Zone− name: String− channel: int− moisture: intValveBoard~ pulse(int, int)WateringPlan− board: ValveBoard− target: int~ water(List<Zone>): int

The class diagram of this version, drawn from its code: the plan owns a board and calls it directly.

board: channel 1 open for 26 s
board: channel 3 open for 10 s
Site A: watered 2 of 3 zones

2 · The change

A second site with other valves

The growers open a second site. Its valves come from another vendor, RainLink, and are opened over HTTP by zone name rather than by channel number. The watering rule is the same.

In this design the rule can't be reused as it is. WateringPlan names ValveBoard, creates it, and speaks its language of channels and pulses:

Where the plan depends on the boardJava
class WateringPlan {
    private final ValveBoard board = new ValveBoard();
    private final int target;

    WateringPlan(int target) {
        this.target = target;
    }

    int water(List<Zone> zones) {
        int watered = 0;
        for (Zone zone : zones) {
            if (zone.moisture() >= target) continue;
            board.pulse(zone.channel(), 2 * (target - zone.moisture()));
            watered++;
        }
        return watered;
    }
}

Supporting RainLink means editing the plan: a second field, a flag for which site it's at, and a branch around every valve call. That's an edit to the growers' rule for a reason that has nothing to do with watering. It isn't the only cost:

What the plan depends on

What that costs

The ValveBoard class, by name

Every new kind of valve edits WateringPlan

The board's own terms: channels and pulses

A vendor whose valves have names instead of channels doesn't fit the plan's calls

A board it creates itself

Testing the rule needs a real board, or a way in that the plan doesn't offer

The direction of the dependency is the problem. The policy, the code that makes the decisions, depends on a detail, the code that talks to hardware. Details change more often than policy, and each change reaches upward.

3 · The fix

Let the plan own an interface, and the boards implement it

Start from the first version. Ask what the plan needs from a valve, in the plan's own words, and make that an interface:

Valves has one method, open(zone, seconds). It speaks in zones and seconds, the plan's terms, not in channels and pulses.

Java
interface Valves {
    void open(Zone zone, int seconds);
}
1«record»Zone− name: String− channel: int− moisture: int«interface»Valves+ open(Zone, int)ValveBoard+ open(Zone, int)WateringPlan− valves: Valves− target: int~ water(List<Zone>): int

Site A prints exactly what it printed before, and site B is watered by the same rule:

board: channel 1 open for 26 s
board: channel 3 open for 10 s
Site A: watered 2 of 3 zones
rainlink: POST /zones/lettuce/open?seconds=16
Site B: watered 1 of 2 zones

The middle step is worth remembering. Adding an interface changed nothing about the dependency while the plan still wrote new ValveBoard(): a class that creates its collaborator depends on it, whatever type its field has.

4 · The principle

Depend on abstractions the policy owns

The dependency inversion principle has two parts:

  • High-level modules shouldn't depend on low-level modules; both should depend on abstractions. WateringPlan and ValveBoard both depend on Valves.
  • Abstractions shouldn't depend on details; details should depend on abstractions. Valves mentions zones and seconds, nothing about relays or HTTP, and each board implements it.
«record»Zone− name: String− channel: int− moisture: int«interface»Valves+ open(Zone, int)ValveBoard+ open(Zone, int)RainLinkValves+ open(Zone, int)WateringPlan− valves: Valves− target: int~ water(List<Zone>): int

After the fix: the plan depends on Valves, and both boards implement it. No arrow leaves the plan toward a board.

What's inverted is the direction of the source code dependency, not the direction of the calls. At run time the plan still calls the board. In the code, the arrow used to run from the plan to the board; now it runs from each board up to the plan's interface.

Aspect

Before

After

Who calls whom at run time

The plan calls the board

The plan calls the board, through Valves

Whose code names whom

The plan names ValveBoard

Each board names Valves; the plan names no board

Where the interface lives

There is none

With the plan, the code that needs it

What a new vendor changes

WateringPlan

Nothing that exists: one new class

Ownership matters. The interface belongs with the policy that uses it. In a larger codebase, WateringPlan and Valves sit in one package and the boards in another; the hardware package imports the watering package, never the reverse. An interface that sits next to ValveBoard and copies its methods would keep the old direction under a new name.

5 · When breaking it is fine

Stable details don't need an interface

The fix has costs:

  • An extra type for every inverted dependency.
  • A level of indirection when reading: jumping to open() lands on the interface, not on the code that runs.
  • Wiring. Something, here main, now has to create the boards and hand them over.

Depend on the concrete class when the detail is stable and you'd never swap it:

  • The standard library. WateringPlan uses a list of zones directly; nobody needs an interface in front of ArrayList.
  • Values. Zone is plain data, and depending on it costs nothing.
  • One implementation, no tests that need a fake. An interface with a single class behind it and no prospect of another is a mirror that has to be kept in step.

Dependency

Invert it?

Why

Hardware, a network service, a vendor SDK

Yes

It changes for reasons the policy doesn't share, and tests need a stand-in

The clock, randomness, the file system

Usually

Tests need to control them

Lists, strings, your own value types

No

Stable, with nothing to swap

6 · Getting it right

Write the interface in the policy's words

  • Name the interface for the need, not the device. Valves.open(zone, seconds), not RelayBoard.pulse(channel, ms).
  • Don't leak details through it. No channel numbers, HTTP status codes or vendor exceptions in the interface; each implementation translates its own.
  • Keep it as small as the policy needs. One method here; see Interface segregation.
  • Move creation out. The policy should neither create nor look up its details. Who does it, and where, is the subject of Dependency injection.
  • Wrap what you can't edit. For a vendor library with its own types, write a small class that implements your interface and calls the library: an adapter.

Each language draws the boundary a little differently:

Language

The policy's interface

How a detail meets it

Java

An interface in the policy's package

implements Valves, in another package that imports the policy's

C++

An abstract class in the policy's header

Derives from it, in its own source file; the policy's files never include the board's header

JavaScript

A documented shape; the policy's module imports no board module

Any object with open()

Python

A Protocol in the policy's module

Any class with open(); it needn't even import the protocol

7 · Look-alikes

Inversion, injection and their relatives

These names are often used as if they meant the same thing. They answer different questions.

Idea

The question it answers

Dependency inversion

Which way should the source code dependency between policy and detail point?

Dependency injection

How does an object get the collaborators it depends on, without creating them?

Inversion of control

Who drives the program: your code, or a framework that calls your code?

Strategy

How does an object switch between ways of doing one job?

Adapter

How does an existing class fit an interface it wasn't written for?

Ports and adapters

How does a whole application keep its core free of frameworks and devices? This principle, applied at the scale of the architecture

The checks recognize the fixed version as Strategy as well as dependency injection, and the shape is the same: a class holding one of several implementations, given from outside. The intent differs. Strategy is about choosing a behavior; dependency inversion is about keeping the policy's code free of its details, even when there's only ever one detail at a time.

8 · In practice

Pricing at the garage

In the parking-lot practice problem, ParkingLot is the policy and pricing is a detail that changes on match days. The lot holds a pricing interface and is given an implementation, so a new pricing scheme never edits the lot.

The checks flag a lot that creates its own pricing with new: the same half-step as the seam version here, an interface on the field and a concrete class behind it. Design the parking lot with the lot depending only on what it owns.

Check yourself

4 questions

1. Where should the Valves interface live?
2. In the middle version, WateringPlan's field has the type Valves. Why do the checks still flag it?
3. RainLink ships a library you can't edit, with startIrrigation(zoneId, duration). How does it fit the design?
4. Which dependency is fine to keep on a concrete class?

Takeaways

Remember this

  • High-level policy and low-level details should both depend on an abstraction.
  • The abstraction belongs to the policy and is written in its terms, not the device's.
  • What's inverted is the source code dependency, not the direction of the calls.
  • Adding an interface isn't enough: a class that creates its collaborator still depends on it.
  • Stable details, such as the standard library and your own values, don't need an interface.