Why The Dependency Inversion Principle Is Worth Using
Tags: programming, beginners, typescript, codequality
Published: 2021-04-01 22:13:51

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eznvnc2jslo1phhycz5a.png)

#### What This Means
:heavy_check_mark: Both High-level and Low-level modules should depend on the same Abstraction.
:heavy_check_mark: High-level modules should implement abstractions that implement Low-Level modules and vice-versa.
:heavy_check_mark: Abstractions can implement different details of the operation.
:x: 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](https://dev.to/huzaifa99/explained-open-close-principle-in-2-minutes-29dd)**. But for this example at the moment we only accept cash payment.
```typescript
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
```typescript
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](https://dev.to/huzaifa99/why-you-should-be-following-the-single-responsibility-principle-1h2a)**.

---
Using **Dependency Inversion**, we will make wrapper classes or abstractions around cash and credit payment implementations.
```typescript
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](https://dev.to/huzaifa99/is-it-practical-to-use-interface-segregation-principle-3ojl)** and **[Liskov Substitution](https://dev.to/huzaifa99/is-the-liskov-substitution-principle-really-useful-3oce)** 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 :sparkling_heart: if you liked it.