Huzaifa Rasheed

Huzaifa Rasheed

Software Engineer

Blogs

Why The Dependency Inversion Principle Is Worth Using

Posted on April 2, 2021

Repost of https://dev.to/rhuzaifa/why-the-dependency-inversion-principle-is-worth-using-opj

Dependency Inversion is the D of SOLID, and you may wonder, what is SOLID?

SOLID are 5 software development principles or guidelines based on Object-Oriented design making it easier for you to make your projects scalable and maintainable.

Rules and Conventions have their place. In code, the SOLID design is considered a convention/best-practice.

Now what is Dependency Inversion?

Basically

High-Level Modules Should Not Depend Upon Low-Level Modules. Both Should Depend Upon Abstractions.

Not too difficult. Right? Here is more

Abstractions Should Not Depend Upon Details. Details Should Depend Upon Abstractions.

Too confusing? This might help

Dependency Inversion Pictorial Example

What This Means

✔️ Both High-level and Low-level modules should depend on the same Abstraction.
✔️ High-level modules should implement abstractions that implement Low-Level modules and vice-versa.
✔️ Abstractions can implement different details of the operation.
❌ High-level modules can implement details surpassing abstractions.

Goal

  • Follow an abstract/facade/wrapper pattern.
  • Hide Low-Level implementation from High-Level Implementation.

A Simple Use Case

We will continue with the payment example from Open/Close Principle. But for this example at the moment we only accept cash payment.

interface Payment {
	pay(): boolean
}

class CashPayment implements Payment {
	public pay(amount){
		// handle cash payment logic
	}
}

function makePayment(amount: number, paymentMethod: Payment){
	if(paymentMethod.pay(amount)){
		return true;
	}
	return false;
}

In this particular example, makePayment() is a high-level module and CashPayment is a low-level module. It clearly has no wrapper/abstraction layer.

Now, what if we want to add credit card payment? We might modify our code like this

interface Payment {
	pay(): boolean
}

// (low-level module)
class CashPayment implements Payment {
	constructor(user){
		this.user = user
	}

	public pay(amount){
		// handle cash payment logic
	}
}

// (low-level module)
class CreditCardPayment implements Payment {
	constructor(user){
		this.user = user
	}

	public pay(amount, creditCardId){
		// handle creditCard payment logic
	}
}


// (High-level Module)
function makePayment(amount: number, paymentMethod: Payment){
  
	if(paymentMethod instanceof CashPayment){
		if(paymentMethod.pay(amount)){
			return true;
		}
	}

	if(paymentMethod instanceof CreditCardPayment){
		if(paymentMethod.pay(amount,paymentMethod.user.creditCardId)){
			return true;
		}
	}
  
  	return false;
}

This clearly violates the Dependency Inversion principle since the high-level module is implementing details for low-level modules. It also violates the Single Responsibility Principle.


Using Dependency Inversion, we will make wrapper classes or abstractions around cash and credit payment implementations.

interface Payment {
	pay(): boolean
}

// (Wrapper/Abstraction around cash payment)
class CashHandler implements Payment {
	constructor(user){
		this.user = user
		this.CashPayment = new CashPayment();
	}

	pay(amount){
		this.CashPayment.pay(amount)
	}
}

// (low-level module)
class CashPayment {
	public pay(amount){
		// handle cash payment logic
	}
}


// (Wrapper/Abstraction around credit card payment)
class CreditCardHandler implements Payment {
	constructor(user){
		this.user = user
		this.CreditCardPayment = new CreditCardPayment();
	}

	pay(amount){
		this.CreditCardPayment.pay(amount, this.user.creditCardId)
	}
}

// (low-level module)
class CreditCardPayment {
	public pay(amount, creditCardId){
		// handle creditCard payment logic
	}
}


// (High-level Module)
function makePayment(amount: number, paymentMethod: Payment){
	if(paymentMethod.pay(amount)){
		return true;
	}
	return false;
}

As we can see now our high-level modules are separated by an abstract layer hiding the details of low-level implementation. We inverted the dependencies.

Why Dependency Inversion is Worth Using?

Consider This

We want to add PayPal and WireTransfer Payment Options to our existing code (the example we just did). It can be done easily without touching our existing code. We only have to add low-level and wrapper implementations for PayPal and WireTransfer Payments. Thus, our High-level implementations never break since we are not touching it.

Also

  • We create resilient and reusable code.
  • Code that is easier to maintain.
  • Easier to test individual code components.
  • We prevent code breakages by not touching high-level implementation

The pattern for other SOLID principles like Interface Segregation and Liskov Substitution is much similar in terms of code breakages i.e : Avoid Them in the long run.


Here it is guys. Do you use Dependency Inversion? Be sure to tell me your opinion in the comments and give this article a Heart 💖 if you liked it.