DEV Community

Cover image for Dependency Inversion Principle
Josué Rodríguez (He/Him)
Josué Rodríguez (He/Him)

Posted on

Dependency Inversion Principle

The Dependency Inversion Principle states that entities must depend on abstractions, not on concretions. High-level modules should not depend on low-level modules. Both should depend on abstraction.

High-level modules should not depend on low-level modules. Both should depend on abstraction.

When we talk about high-level modules we are referring to a class that executes an action implementing a tool or library, and when we talk about low-level modules we are referring to the tools or libraries that are needed to execute an action.

The principle allows for decoupling, which means to separate, disengage or dissociate something from something else. This helps us by reducing dependency and allowing for easier implementations of other tools in the future.

Example

Let's imagine that we have a Candy Store and we are developing the checkout process. In the beginning, we only planned to implement Stripe as our payments processor. Stripe needs for the amount to be passed on as cents to make the transaction. Our classes will look something like this:

//Checkout.js
class Checkout {
  constructor() {
    this.paymentProcessor = new Stripe('USD');
  }

  makePayment(amount) {
    //Multiplying by 100 to get the cents
    this.paymentProcessor.createTransaction(amount * 100);
  }
}

//Stripe.js
//Custom Stripe implementation that calls the Stripe API
class Stripe {
  constructor(currency) {
    this.currency = currency;
  }

  createTransaction(amount) {
    /*Call the Stripe API methods*/
    console.log(`Payment made for $${amount / 100}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that we created a dependency between our Checkout class (high-level module) and Stripe (low-level module), violating the Dependency Inversion Principle. The dependency is especially noticeable when we convert the amount to cents. The Checkout should not care about which payment processor is being used, it only cares about making a transaction.

To decouple these two modules, we would have to implement an intermediary between the checkout and the payment processor, creating an abstraction so that no matter what payment processor we use, the Checkout class will always work with the same method calls. The new PaymentProcessor class will be in charge of adapting everything to payment processor to be used (in this case, Stripe). The intermediary class will have the following code:

//PaymentProcessor.js
class PaymentProcessor {
  constructor(processor, currency) {
    this.processor = processor;
    this.currency = currency;
  }

  createPaymentIntent(amount) {
    const amountInCents = amount * 100;
    this.processor.createTransaction(amountInCents);
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the createPaymentIntent on the PaymentProcessor class is converting the amount to cents. And now we refactor the Checkout class to implement the abstraction:

//Checkout.js
class Checkout {
  constructor(paymentProcessor) {
    this.paymentProcessor = paymentProcessor;
  }

  makePayment(amount) {
    this.paymentProcessor.createPaymentIntent(amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, if we ever need to change our payment processor, we can do so by passing the new processor instead of Stripe to the PaymentProcessor constructor. Then, we pass the PaymentProcessor to the Checkout:

//index.js
const paymentProcessor = new PaymentProcessor(new Stripe('USD'), 'USD');
const checkout = new Checkout(paymentProcessor);
Enter fullscreen mode Exit fullscreen mode

Imagine that now we are asked to replace Stripe with another payment processor that does not require for the amount to be converted to cents but on every transaction asks for the currency that's going to be used. The resulting code will be the following:

//Checkout.js
class Checkout {
  constructor(paymentProcessor) {
    this.paymentProcessor = paymentProcessor;
  }

  makePayment(amount) {
    this.paymentProcessor.createPaymentIntent(amount);
  }
}

//PaymentProcessor.js
class PaymentProcessor {
  constructor(processor, currency) {
    this.processor = processor;
    this.currency = currency;
  }

  createPaymentIntent(amount) {
    this.processor.createTransaction(amount, this.currency);
  }
}

//BetterProcessor.js
class BetterProcessor {
  createTransaction(amount, currency) {
    console.log(`Payment made for ${amount} ${currency}`);
  }
}

//index.js
const paymentProcessor = new PaymentProcessor(new BetterProcessor(), 'USD');
const checkout = new Checkout(paymentProcessor);
Enter fullscreen mode Exit fullscreen mode

Notice how we only changed the processor passed to the PaymentProcessor class and how the Checkout class remained untouched. We adapted the intermediary class PaymentProcessor to the processor needs.

We removed the dependency between Checkout and the processor used by implementing the intermediary class PaymentProcessor, following the Dependency Inversion Principle.

Latest comments (0)