Low-level design2 min

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) states two things:

  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 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.

typescript
// 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.

typescript
// 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.