DEV Community

Cover image for Why The Dependency Inversion Principle Is Worth Using
Huzaifa Rasheed
Huzaifa Rasheed

Posted on

Why The Dependency Inversion Principle Is Worth Using

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
thunderclap1030 profile image
Dat Pham

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 the Interface Segregation Principle and Open/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