The dependency inversion principle — who owns the abstraction
DIP is the principle whose three terms are most easily confused: dependency inversion (DIP), dependency injection (DI), and inversion of control (IoC). The initials look alike, they appear in the same contexts, and they refer to different things.
Confuse them and a concrete failure follows: you switch to receiving dependencies through the constructor, conclude that you have satisfied DIP, and nothing about the direction of dependencies has changed. This chapter confirms in the original text what inverts, and separates the three terms.
What the original said — DIP is the means of achieving OCP
Martin's 2000 document defines DIP this way1.
Depend upon Abstractions. Do not depend upon concretions.
It then states the relationship to the principles of the previous chapters.
If the OCP states the goal of OO architecture, the DIP states the primary mechanism.
DIP is positioned not as an independent goal but as the means of achieving OCP. That corresponds to the statement seen in chapter 3 that "abstraction is the key to the OCP."
What actually "inverts"
The original explains the substance of the inversion by contrast with procedural design.
Procedural designs exhibit a particular kind of dependency structure. ... this structure starts at the top and points down towards details. High level modules depend upon lower level modules, which depend upon yet lower level modules
Against that, the object-oriented structure looks like this.
the modules that contain detailed implementation are no longer depended upon, rather they depend themselves upon abstractions. Thus the dependency upon them has been inverted.
What inverts is the arrow of dependency between the higher and the lower level. What had the higher pointing at the lower changes into the lower pointing at an abstraction.
The structure of the diagram the original supplies shows this direction. The high-level policy points at an abstract interface, and the detailed implementation points at the abstract interface too. The arrows converge on the abstraction.
Sorting out DIP, DI, and IoC
Let us separate the three terms.
| Term | What level it is about | Satisfying it does not imply satisfying the others |
|---|---|---|
| IoC (inversion of control) | A general style of handing the initiative for control from the callee to the framework | IoC is broader than DI. Callbacks and template methods are IoC too |
| DI (dependency injection) | An implementation technique for passing the objects you depend on in from outside | Injecting a concrete class is still DI |
| DIP (the dependency inversion principle) | A design principle about the direction of dependency in the source code | Nothing is inverted unless the abstraction belongs to the higher-level side |
Martin Fowler writes that he chose the term "dependency injection" precisely to make this distinction clear2.
Inversion of control is a common characteristic of frameworks, so saying that these lightweight containers are special because they use inversion of control is like saying my car is special because it has wheels.
Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.
"It is better because it uses IoC" says next to nothing — that is Fowler's point. Use a framework and control is usually inverted already.
Why follow it — not getting dragged along by what changes
What DIP reduces is the cost of a dependency on something volatile propagating upward.
Business rules outlive storage destinations and notification mechanisms. The rule "a patron may borrow up to five items" stays the same whether the database is swapped out or the way notices are sent changes. Yet if the higher level calls the lower level directly, you end up opening the higher level every time the lower level changes.
Interpose an abstraction and all the higher level sees is the contract "this can be saved." As long as the contract does not change, the higher-level code keeps working no matter how many times the lower level is rebuilt.
What is doing the work here is not the shape called abstraction but the contract changing less often than the lower level does. That is also why the original puts frequency of change at the center of the criterion. Whether to follow it therefore depends on whether what you depend on actually changes. That judgment is handled below under "When it's OK to break it."
Who owns the abstraction
This is the heart of DIP, and the place where the difference from DI shows up most clearly.
Even when you pass dependencies in from outside, whether an inversion has happened depends on which module the abstraction belongs to.
The code examples below handle loan records in this form.
// A different shape from chapter 4's LendingRecord. Here it is treated as the target of persistence
type LendingRecord = {
readonly id: string;
readonly borrowerId: string;
readonly itemId: string;
readonly lentAt: Date;
};
// A file in the infrastructure layer (storage/lending-record-store.ts)
export interface LendingRecordStore {
save(record: LendingRecord): void;
findByBorrower(borrowerId: string): readonly LendingRecord[];
}
export class SqlLendingRecordStore implements LendingRecordStore {
save(record: LendingRecord): void {}
findByBorrower(borrowerId: string): readonly LendingRecord[] {
return [];
}
}
// A file in the business logic layer (usecase/lend-item.ts)
import type { LendingRecordStore } from "../storage/lending-record-store";
export class LendItem {
constructor(private readonly store: LendingRecordStore) {}
// ^^^^^^^^^^^^^^^^^^
// An interface, but defined in the infrastructure layer.
// Business logic imports the infrastructure layer = the direction of dependency is unchanged
}
LendItem depends on an interface, and the implementation is injected through the constructor. As DI it is correct; as DIP it has achieved nothing. The import arrow still points from business logic to the infrastructure layer. Change the interface to suit the infrastructure layer and the business logic gets dragged along.
Move the abstraction to the higher-level side and the direction changes.
// A file in the business logic layer (usecase/lend-item.ts)
export interface LendingRecordStore {
save(record: LendingRecord): void;
findByBorrower(borrowerId: string): readonly LendingRecord[];
}
export class LendItem {
constructor(private readonly store: LendingRecordStore) {}
}
// A file in the infrastructure layer (storage/sql-lending-record-store.ts)
import type { LendingRecordStore } from "../usecase/lend-item";
// ^^^^^^^^^^^^^^^^^^^^
// The infrastructure layer imports the business logic layer = the dependency is inverted
export class SqlLendingRecordStore implements LendingRecordStore {
save(record: LendingRecord): void {}
findByBorrower(borrowerId: string): readonly LendingRecord[] {
return [];
}
}
The code barely looks different. All that changed is the file the interface sits in. The meaning is nonetheless reversed. The infrastructure layer now conforms to a contract the business logic decided, and the needs of the implementation no longer leak upward.
The test is simple. Look at which way the import arrow points. If the higher level imports the lower level, nothing is inverted.
The original states this placement explicitly where it explains the technique for breaking cycles between packages.
Notice the placement of BY. It is placed in the package with the class that uses it. ... Interfaces are very often included in the package that uses them, rather than in the package that implements them.
An interface belongs not to the implementing side but to the using side — that is the original's position.
Note that this repo's PHP Class Design Guide — Interfaces and Depending on Abstractions explains inversion as "replacing the form where the higher level depends directly on the lower level with a form where both depend on an abstraction." This chapter goes further, into which module that abstraction belongs to. Even when both depend on an abstraction, if the abstraction sits on the lower-level side the direction of dependency has not changed.
How it gets misused
Believing that injecting means inverting. As seen above. Registering with a DI container is the same: registration is about assembly and has nothing to do with the direction of dependency.
Mechanically creating abstractions that have only one implementation. The same failure as chapter 3's predictive abstraction. DIP reads as "make every dependency an abstraction." The original does hold that up as the ideal, while at the same time admitting it is too strict and writing that there are circumstances that should soften it (the misspelling is as in the original).
Clearly such a restriction is draconian, and there are mitigating circumstatnces that we will explore momentarily. But, as much as is feasible, the principle should be followed.
The original goes on to explain the reason to follow it in terms of frequency of change.
The reason is simple, concrete things change alot, abstract things change much less frequently.
The criterion is not "is it concrete" but "does it change." Draping an abstraction over a concrete thing that does not change protects nothing.
When it's OK to break it
When what you depend on is stable. The original grants this exception a section of its own as a "mitigating circumstance."
the
string.hstandard C library is very concrete, but is not at all volatile. Depending upon it in an ANSI string environment is not harmful. Likewise, if you have tried and true modules that are concrete, but not volatile, depending upon them is not so bad.
The original adds a caution, however.
Non-volatility is not a replacement for the substitutability of an abstract interface.
If you have reasons to substitute beyond testing, being stable is not a reason to skip the abstraction.
When substitution is not actually happening. Check whether you genuinely need to substitute for tests. Things you want to control in tests, such as dates and random values, are the classic cases with a track record of substitution. On the other hand, if you have interposed an abstraction over a dependency you have never once substituted, that abstraction is only paying cost of carry.
When there is no module boundary. What DIP protects is the direction of dependency across modules. A dependency that stays inside one file or one small module has no boundary to invert.
Where TypeScript stands today
In TypeScript, an interface is not the only means of expressing an abstraction.
// The business logic layer declares only the shape it needs
type SaveLendingRecord = (record: LendingRecord) => void;
type Now = () => Date;
export function lendItem(
save: SaveLendingRecord,
now: Now,
record: LendingRecord,
): void {
save({ ...record, lentAt: now() });
}
When you need one operation, you need neither an interface nor a class. Write the contract as a function type and you control the direction of dependency just the same. In tests you substitute by passing a function.
It is the same idea as ISP in the previous chapter. The client side declares the shape it needs, and the implementing side conforms. Whether to create a named abstraction comes down to whether you want to name the contract.
Reading a type with import type also has the property of creating no runtime dependency. This is not a substitute for DIP, however. Even with no runtime dependency, the direction of dependency in the source code is unchanged. What DIP addresses is the structure of the source.
Summary
- Martin positions DIP as the primary means of achieving OCP. It is not an independent goal
- What inverts is the direction of dependency between the higher and lower level. The side holding the details changes to depending on an abstraction
- IoC, DI, and DIP are separate concepts. Fowler chose the term dependency injection because IoC was too generic
- Injecting a dependency does not invert anything if the abstraction sits on the lower-level side. The test is which way the
importarrow points - The original itself admits that "everything an abstraction" is too strict. The criterion is not whether it is concrete but whether it changes
- In TypeScript you can express an abstraction with a function type. A named interface is what you choose when you want to name the contract
What to read next
The final chapter covers conflicts among the principles and the limits of SOLID. It handles the situations where the five principles pull against each other, and how to decide which to take.
Exercises
The following code uses DI. Does it satisfy DIP? Answer by tracing the ownership of the abstractions
The file layout is as follows.
// infra/system-clock.ts
export interface Clock {
now(): Date;
}
export class SystemClock implements Clock {
now(): Date {
return new Date();
}
}
// infra/id-generator.ts
export interface IdGenerator {
next(): string;
}
// usecase/register-loan.ts
import type { Clock } from "../infra/system-clock";
import type { IdGenerator } from "../infra/id-generator";
export class RegisterLoan {
constructor(
private readonly clock: Clock,
private readonly idGenerator: IdGenerator,
) {}
execute(borrowerId: string, itemId: string): LendingRecord {
return {
id: this.idGenerator.next(),
borrowerId,
itemId,
lentAt: this.clock.now(),
};
}
}
Sample answer
It satisfies DI but not DIP.
RegisterLoan does not construct concrete classes directly; it receives Clock and IdGenerator through the constructor. That is dependency injection.
Look at the import arrows, however, and the business logic layer (usecase/) points at the infrastructure layer (infra/), because the abstractions are placed in the infrastructure layer. The direction of dependency is the same as in procedural design; nothing is inverted.
As a concrete inconvenience, add an infrastructure-driven method such as "return the time zone" to Clock in infra/system-clock.ts and the business logic side gets dragged along by that change. The infrastructure layer is deciding the shape of the abstraction.
The fix is only to change where the interfaces live. Move the declarations of Clock and IdGenerator to the usecase/ side and have the implementation classes in infra/ import usecase/. Not one line inside the classes changes.
Note that in TypeScript these two abstractions do not even need to be interfaces.
// usecase/register-loan.ts
type Now = () => Date;
type NextId = () => string;
export function registerLoan(
now: Now,
nextId: NextId,
borrowerId: string,
itemId: string,
): LendingRecord {
return { id: nextId(), borrowerId, itemId, lentAt: now() };
}
With one operation each, function types are enough. In tests you substitute by passing a function, and the import of infra/ disappears.
Summarizing the procedure for deciding: (1) see which file the abstraction is in (2) see whether what imports that file is the higher or the lower level (3) if the higher level imports the lower level, nothing is inverted even though you are injecting.