Dependency Inversion Principle (DIP)
When to Use
When designing layered architectures, building testable systems, or decoupling high-level business logic from low-level implementation details.
Decision: What DIP Means
Robert Martin's Definition: 1. High-level modules should not depend on low-level modules. Both should depend on abstractions. 2. Abstractions should not depend on details. Details should depend on abstractions.
In practice: Depend on interfaces/contracts, not concrete implementations. Inject dependencies rather than creating them.
| If you... | Problem | Solution |
|---|---|---|
new DatabaseConnection() inside business logic |
Tight coupling to DB | Inject IDataStore interface |
| Import concrete classes from lower layers | Architectural violation | Depend on abstractions |
| Can't test without real database | Untestable | Mock interface, not implementation |
| Change in low-level detail breaks high-level code | Fragile architecture | Invert dependency direction |
Pattern: DIP Violation
PHP Example:
// Violation: high-level depends on low-level concrete class
class OrderProcessor {
private MySQLDatabase $database;
public function __construct() {
$this->database = new MySQLDatabase(); // VIOLATION: direct instantiation
}
public function processOrder(Order $order) {
$this->database->save($order); // Coupled to MySQL
}
}
// Can't switch to PostgreSQL without changing OrderProcessor
// Can't test OrderProcessor without real MySQL
DIP-Compliant:
// Abstraction: both layers depend on this
interface DataStore {
public function save(Order $order): void;
}
// High-level module depends on abstraction
class OrderProcessor {
private DataStore $database;
public function __construct(DataStore $database) {
$this->database = $database; // Dependency injection
}
public function processOrder(Order $order) {
$this->database->save($order);
}
}
// Low-level module implements abstraction
class MySQLDatabase implements DataStore {
public function save(Order $order): void {
// MySQL-specific implementation
}
}
// Easy to switch or test
$orderProcessor = new OrderProcessor(new MySQLDatabase());
// Or: new OrderProcessor(new PostgreSQLDatabase());
// Or (test): new OrderProcessor(new MockDataStore());
Pattern: Dependency Injection
TypeScript Example:
// Without DI (violation)
class UserService {
private emailSender = new SMTPEmailSender(); // Tight coupling
register(user: User) {
this.emailSender.send(user.email, "Welcome!");
}
}
// With DI (DIP-compliant)
interface EmailSender {
send(to: string, message: string): void;
}
class UserService {
constructor(private emailSender: EmailSender) {}
register(user: User) {
this.emailSender.send(user.email, "Welcome!");
}
}
class SMTPEmailSender implements EmailSender {
send(to: string, message: string): void { /* SMTP logic */ }
}
// Usage
const userService = new UserService(new SMTPEmailSender());
// Or: new UserService(new SendGridEmailSender());
// Or (test): new UserService(new MockEmailSender());
DIP vs Dependency Injection
DIP (principle): Depend on abstractions, not concretions. DI (pattern): Inject dependencies from outside rather than creating them inside.
DI enables DIP: Injection allows swapping implementations that satisfy the abstraction.
Common Mistakes
- Creating interfaces for every class -- Wait until you need swappability. Why: YAGNI, premature abstraction
- Anemic interfaces -- Interface that mirrors one concrete class exactly. Why: no actual abstraction
- Depending on frameworks -- Business logic importing framework classes directly. Why: framework changes break business logic
- Service Locator anti-pattern -- Static
ServiceLocator.get(IDatabase)hides dependencies. Why: makes dependencies invisible, hurts testability - New keyword in business logic --
new EmailSender()inside service. Why: couples to concrete implementation - Not inverting at architectural boundaries -- DIP matters most at layer boundaries (business <-> persistence, core <-> infrastructure). Why: that's where coupling causes most pain