Software Architecture & Design Principles
Dependency Inversion & Injection
Three related but distinct ideas get conflated under "DI": the Dependency Inversion Principle (depend on abstractions), Dependency Injection (a technique for supplying those abstractions from outside), and an IoC container (one optional tool for wiring injections automatically). You can have the first two without ever touching a container.
- DIP: high-level modules should not depend on low-level modules — both should depend on abstractions
- Dependency Injection: a class receives its collaborators (via constructor, ideally) rather than constructing them itself
- An IoC container automates wiring injections but is a convenience, not a requirement — "poor man's DI" (plain constructors) works fine at small scale
- Constructor injection over field/setter injection: dependencies become explicit, final, and impossible to forget
- DI is what makes swapping a real collaborator for a test double trivial — the core enabler behind Testing Philosophy
// Before: OrderService is welded to a concrete, volatile detail.
class OrderService {
private final SmtpMailer mailer = new SmtpMailer(); // hard-coded, untestable
void placeOrder(Order o) { /* ... */ mailer.send(o.confirmationEmail()); }
}
// After: depends on an abstraction; the concrete choice is supplied from outside.
interface Notifier { void send(String message); }
class OrderService {
private final Notifier notifier;
OrderService(Notifier notifier) { this.notifier = notifier; } // constructor injection
void placeOrder(Order o) { /* ... */ notifier.send(o.confirmationEmail()); }
}| Term | What it is | Required? |
|---|---|---|
| DIP | A design principle: depend on abstractions, not concretions | Foundational — the goal |
| Dependency Injection | A technique: pass collaborators in instead of constructing them | The mechanism — usually via constructor |
| IoC Container | A tool that constructs the object graph and injects automatically | Optional — helpful at scale, unnecessary for a small app |