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
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.
Top comments (1)
I think in section
what if we want to add credit card payment? We might modify our code like this
not violates the
Dependency Inversion
a instead of it violates theInterface Segregation Principle
andOpen/Closed Principle
🤔According to with example, you break down "Payment" interfaces into more granular and specific ones. Clients should implement only those methods that they really need and don't depend on methods they do not use. So that mean you already resolve problem with
Interface Segregation Principle