SOLID Dependency inversion principle

According to this principle, Dependency Inversion Principle (DIP), there should be no dependencies between modules, especially between low-level and high-level modules.

This principle about the inversion of dependencies allows us to decouple different modules of software.

In other words, our software should not depend, for example, on how frameworks are implemented for database access or server connections.

For this purpose, we should use interfaces without the need to know what exactly happens behind the implementation of such interfaces.

We want the following:

  • The classes in our code do not depend directly on low-level classes.
  • Use abstractions through interfaces and only rely on these abstractions.

Poorly implemented code where we do NOT use this dependency inversion principle

We have this Cash class that receives a Product.

Then, within the class Cash the database class is created to persist the product.

Note that we are directly using the Database with its implementation.

We are using low-level classes in our code MySqlDatabase. If later we want to migrate to another database, we should modify our code as well.

class Cash {

   public void pay(Product product) {

        MySqlDatabase persistence = new MySqlDatabase();
        persistence.save(product);

   }

}

class MySqlDatabase {

    void save(Product product) {
        // save into MySqlDatabase...
    }

}

In this example, the Cash class depends on the concrete implementation of the MySqlDatabase class. This breaks the Dependency Inversion Principle.

Running this code

public class Demo {

    public static void main(String[] args) {
        Cash cash = new Cash();
        cash.pay(new Product());
    }

}

How to implement the Dependency inversion principle

The first thing we have to do is stop depending on the particular class MySqlDatabase, so we are going to create an interface that decouples it from persistence.

We do not need to know in this part of our code how or where the product is stored.

To refactor the code and apply DIP, we need to introduce an abstraction. We’ll create an interface called Persistence that abstracts the save method.

We create our interface for persistence:


interface Persistence {

    void save(Product product);

}

We implement the interface in our class MySqlDatabase


class MySqlDatabase implements Persistence {

    public void save(Product product) {
        System.out.println("Save product " + product);
        // save into MySqlDatabase...
    }

}

Our Cash class will now receive the Persistence interface in the constructor.

The class no longer needs to know who or how the persistence is implemented.
The Cash class uses the interface and is unaware of its implementation.

We are going to modify the Cash class to depend on the abstraction (Persistence) instead of the concrete implementation (MySqlDatabase):


class Cash {

    Persistence persistence;

    public Cash(Persistence persistence) {
        this.persistence = persistence;
    }

    public void pay(Product product) {

        persistence.save(product);

    }

}

Now, the Cash class is no longer coupled to the specific implementation of the MySqlDatabase class; it only depends on the Persistence interface.

Running this code

public class Demo {

    public static void main(String[] args) {
        // the Cash class is no longer coupled to the specific implementation 
        // of the MySqlDatabase class
        Persistence persistence = new MySqlDatabase();
        Cash cash = new Cash(persistence);
        cash.pay(new Product());
    }

}

Conclusion

Software designers can use the Dependency Inversion Principle to create more flexible, testable, and reusable systems, making it easier to adapt to changing requirements and maintain software quality over time.

We have several advantages:

  • Flexibility and modularity: By depending on abstractions rather than concrete implementations, DIP allows for easier modification and substitution of components without affecting the rest of the system
  • Testability: DIP promotes effective testing by allowing dependencies to be easily mocked or stubbed.
  • Code reusability: DIP enhances code reusability by decoupling high-level modules from low-level modules.

You can review these examples at GitHub

Hi! If you find my posts helpful, please support me by inviting me for a coffee :)

See also