Dependency Inversion Principle (DIP)
Depend on abstractions, not concrete implementations β both sides of a boundary point at an interface, not at each other.
Intent & Description
π― Intent
High-level business logic directly coupled to a specific database, API, or service can’t be tested, swapped, or evolved without changing the business code itself.
π Context
OrderProcessor instantiates MySQLDatabase directly. You can’t test OrderProcessor without a real database. Switching to MongoDB means editing the business logic class. The high-level policy is at the mercy of a low-level implementation detail.
π‘ Solution
Define an interface (Database). OrderProcessor depends on the interface. MySQLDatabase and MongoDBDatabase both implement it. The concrete implementation is injected at runtime. Now both layers point at the abstraction β neither layer knows the other exists.
Real-world Use Case
π TL;DR
Depend on interfaces, not implementations. Decouples layers, enables mocking, and makes the codebase resilient to low-level change. The foundation of dependency injection.
Advantages
- Loose coupling β swap any implementation without touching the caller
- Easy to test with mocks or stubs injected at any boundary
- High-level business logic is insulated from low-level churn
Disadvantages
- More indirection β every dependency adds an interface and an injection point
- Requires upfront abstraction design β wrong abstractions are expensive to reverse
- Overkill for simple scripts or throwaway code with no testing requirement
// Before: Direct dependency on concretion
class OrderProcessor {
constructor() {
this.database = new MySQLDatabase(); // Direct dependency
}
processOrder(order) {
this.database.save(order);
}
}
// After: DIP applied
class Database {
save(data) {
throw new Error('Must implement save');
}
}
class MySQLDatabase extends Database {
save(data) {
console.log(`Saving to MySQL: ${data}`);
}
}
class MongoDBDatabase extends Database {
save(data) {
console.log(`Saving to MongoDB: ${data}`);
}
}
class OrderProcessor {
constructor(database) {
this.database = database; // Depends on abstraction
}
processOrder(order) {
this.database.save(order);
}
}
// Can easily swap implementations
const processor1 = new OrderProcessor(new MySQLDatabase());
const processor2 = new OrderProcessor(new MongoDBDatabase());