Low-level design2 min
Dependency Inversion Principle
The Dependency Inversion Principle (DIP) states two things:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
In traditional programming, high-level business logic often creates and depends directly on low-level utility classes (like database connections). DIP "inverts" this by putting an interface in the middle.
Real-Life Analogy
Think of a Lamp and an Electrical Outlet. The Lamp (High-level module) does not have its wires soldered directly into the power grid (Low-level module). Instead, both depend on an Abstraction: the standard wall socket interface. You can plug the lamp into any wall in the house, and the house can provide power to any device that fits the socket.
Example: Violation of DIP
Here, the high-level Store class depends directly on the low-level concrete StripeAPI class.
// Low-level module
class StripeAPI {
public charge(amount: number): void {
console.log(`Charged $${amount} via Stripe.`);
}
}
// High-level module
class Store {
private stripe: StripeAPI;
constructor() {
// Tight coupling! The Store creates the concrete dependency itself.
this.stripe = new StripeAPI();
}
public purchase(amount: number): void {
this.stripe.charge(amount);
}
}
If we want to switch to PayPal, we have to rip open the Store class and rewrite it.
Example: Proper DIP Design
We introduce an interface (PaymentProcessor) in the middle. The Store depends on the interface, and StripeAPI implements the interface.
// The Abstraction (The Wall Socket)
interface PaymentProcessor {
charge(amount: number): void;
}
// Low-level module depends on abstraction
class StripeAPI implements PaymentProcessor {
public charge(amount: number): void {
console.log(`Charged $${amount} via Stripe.`);
}
}
class PayPalAPI implements PaymentProcessor {
public charge(amount: number): void {
console.log(`Charged $${amount} via PayPal.`);
}
}
// High-level module depends on abstraction
class Store {
private processor: PaymentProcessor;
// Dependency Injection: The concrete implementation is passed in!
constructor(processor: PaymentProcessor) {
this.processor = processor;
}
public purchase(amount: number): void {
this.processor.charge(amount);
}
}
// Usage
const stripeStore = new Store(new StripeAPI());
stripeStore.purchase(50);
// Switching to PayPal requires ZERO changes to the Store class!
const paypalStore = new Store(new PayPalAPI());
paypalStore.purchase(50);
Why is it Important?
- Testability: You can easily inject a "Mock" or "Fake" implementation of a dependency during unit testing.
- Flexibility: You can swap out databases, UI frameworks, or payment processors without touching your core business logic.