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}`);
}
}
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);
}
}
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);
}
}
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);
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);
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.
Top comments (0)