Dependency Inversion Principle (DIP)

Dependency Inversion Principle (DIP) is the last principle among the SOLID principles of the Object-Oriented Programming (OOP) model that we have seen in the last article. Let’s recall what Dependency Inversion Principle (DIP) talks about as follows:

  • High-level modules should not depend on low-level modules, and both should depend on abstraction.
  • Details or concrete implementation should depend on abstraction rather than an abstraction that depends on details.

DIP is a design principle that provides a suggestion or guideline about the right and wrong way of designing the application. It doesn’t give a concrete solution. In short, DIP just talks about what to do instead of how to do it.

In this article, we will discuss the Dependency Inversion Principle (DIP) in more detail.

Dependency Inversion Principle – High-level and Low-level modules

Before jumping into the main topic, let’s understand what high-level and low-level modules are. In the anatomy of software development methodology, we often build a large system that comprises small, independent, discrete, and self-contained components called modules.

Each module in the system is designed for specific functionality. It is capable of performing a task in a self-sufficient manner. These modules are the basic building blocks of the entire application. This is what we called modularization.

Above all, the module comprises various elements. These elements work in a combined manner to form a module as a single unit. The elements of modules work together, and the module itself may interact with other modules to execute the task that spans multiple modules.

Cohesion and Coupling

This design brings the two most important concepts in the software design paradigm as follows.

  • Cohesion: It’s a measurement of interaction that happens between the elements within the module.
  • Coupling: It’s an amount of interdependency between the modules to achieve a specific task.

The cohesion can be correlated with the amount of responsibility in a real scenario. It’s an obligation towards the specific task that we have identified for a module to act upon.

Modules must have a high cohesion design. High cohesion means the module is well focused on a specific task to accomplish. It makes the module fine-grained, well dedicated, robust, and reusable. In a single sentence, high cohesion means “Do one thing only but do it well.”

Coupling, on the other hand, is a level of dependency a module has on other modules. Modular applications consist of modules, each of which serves a specific functionality.

Obviously, it has to rely on other modules to perform a task for which it’s not designed. This is where the coupling comes into the picture. You might have got the clue that, to make the system less fragile, you must reduce and manage the coupling in a proper way.

When modules are interdependent, one should make sure the changes introduced in one module should not break the others. In short, the coupling should be defined with a well-established interface between the modules to reduce the ripple impact of changes.

The Dependency Inversion Principle moves around these two concepts.

Modules executing a core part of the application are called high-level modules. The high-level modules rely on other modules to perform supporting functions. They are called low-level modules.

Changes are part of the software development cycle. If not applied in a proper manner, they produce a maintenance nightmare down the line. Changes become risky, especially with dependent code.

Anatomy of Dependency Inversion Principle

This is what DIP talks about – keep the chunk of dependent code (low-level module) away from the main program (high-level module), which is not directly associated.

In other words, DIP suggests eliminating the direct dependency of the low-level module on high-level modules to reduce the coupling. Rather, they must rely on some sort of abstraction so that the high-level module can define a generic low-level behavior.

DIP helps avoid the ripple effect of implementation changes in the low-level module on the high-level module. This ultimately brings flexibility and adaptivity to the application. So long as the low-level module aligns with the abstraction, the high-level module can utilize it without any code changes.

Dependency Inversion Principle with example

Let’s look at a real-life example and see how DIP can significantly improve the overall application’s flexibility.

Consider a scenario where you are designing a BI (Business Intelligence) system for a departmental store. This application aims to gather, analyze, incorporate, and present business information in various formats.

Typically, you need to fetch data from the database, process it with complex logic, and show it on the screen. If this is implemented with a procedural development style, the system’s flow would be similar to the following diagram.

Dependency Inversion Principle - BI system with procedural way

A single module does all the job – fetching data from DB, processing it with business logic, and exporting it on screen. This is not an optimal design.

Modular design

Let’s break the whole functionality into multiple modules based on their responsibility as follow:

Dependency Inversion Principle - BI system with modular architecture

  • Import DB data module: Responsible for obtaining data from the relational database.
  • Export HTML module: Export the processed data into HTML format.
  • Data Analysis module: Taking data from the import data module, process it with complex business rules, and deliver it to the export data module.

Considering the responsibility, the Data analysis module is a high-level module, while Import Data and Export Data are low-level modules.

The Import DB data module should look similar to the following code snippet.

public class ImportDBData {

    public List<Object[]> importDataFromDatabase() {
        List<Object[]> dataFromDB = new ArrayList<Object[]>();
        // Logic to import data from the database.
        return dataFromDB;
    }
}

The Export HTML module will take the list of data and export it to HTML format. The code should look as follows:

public class ExportHTML {
    public File exportToHTML(List<Object[]> dataLst) {
        File outputHTML = null;
        // Logic to iterate the data list and generate HTML content.
        return outputHTML;
    }
}

The Data Analysis module takes the data from the Import DB data module, which applies the business logic and sends the processed data to the Export HTML module. Its code looks as follows:

public class DataAnalysis {

    private ExportHTML exportHTML = new ExportHTML();
    private ImportDBData importDBData = new ImportDBData();

    public void generateBalanceSheet() {
        List<Object[]> dataFromDB = importDBData.importDataFromDatabase();
        exportHTML.exportToHTML(dataFromDB);
    }
}

Further improvement

Cool ..!!! This seems good design as we have separated out the module based on their responsibility. However, good design can sustain any future changes without breaking the system.

Is this design capable of handling the changes? Will this design make our application fragile while accommodating any amendments?

Let’s find the possibilities of changes in low-level modules. Assume that, over a period of time, you might need to fetch the data from the Web service along with the Database. Similarly, you need to export the data in Excel format along with HTML.

To accommodate the new requirement, you need to create new classes/ modules to fetch data from web service and export it into Excel sheet format as follows:

public class ImportWebServiceData {

    public List<Object[]> importDataFromWebService() {
        List<Object[]> dataFromWebService = new ArrayList<Object[]>();
        // Logic to import data from web service.
        return dataFromWebService;
    }
}

public class ExportExcel {

    public File exportToExcel(List<Object[]> dataLst) {
        File outputExcel = null;
        // Logic to iterate the data list and generate Excel file.
        return outputExcel;
    }
}

Let’s integrate the new low-level modules into high-level modules. But at this time, since we have multiple import and export data modules, we need some sort of flag mechanism. We will utilize the low-level modules inside the high-level module based on the flag we pass. The updated code of the Data Analysis module looks as follows:

public class DataAnalysis {

    private ExportHTML exportHTML = new ExportHTML();
    private ExportExcel exportExcel = new ExportExcel();
    private ImportDBData importDBData = new ImportDBData();
    private ImportWebServiceData importWSData = new ImportWebServiceData();

    public void generateBalanceSheet(int inputMethod, int outputMethod) {

        List<Object[]> dataLst = null;
        if (outputMethod == 1) {
            if (inputMethod == 1) {
                dataLst = importDBData.importDataFromDatabase();
            } else {
                dataLst = importWSData.importDataFromWebService();
            }
            exportHTML.exportToHTML(dataLst);
        } else if (outputMethod == 2) {
            if (inputMethod == 1) {
                dataLst = importDBData.importDataFromDatabase();
            } else {
                dataLst = importWSData.importDataFromWebService();
            }
            exportExcel.exportToExcel(dataLst);
        }

    }
}

Resolve design problem with Dependency Inversion Principle

Great work ..!! The high-level module is flexible enough to deal with two different input and output methods to generate the data analysis report.

But hold on. What happens when a few more input and output methods are introduced? Let say you need to fetch the data from Google drive and export it into PDF format.

For every new low-level module, we need to keep updating the code in the high-level module. This is because the high-level module is closely dependent on the concrete implementation of the low-level module.

In other words, there are tight couplings between low-level and high-level modules. This breaks another fundamental design principle – Open for extension and close for modification.

Now, it’s time to apply the Dependency Inversion Principle. Let’s recall what DIP says: High-level modules should not depend on low-level modules for their responsibilities. Both should depend on abstraction.

This was the core problem in our design. The high-level module (Data analysis) is tightly coupled with low-level modules (import and export data).

Limitation of Dependency Inversion Principle

Unfortunately, Design principles talk about the solution to the given problem. They remain silent about how to implement it. DIP suggests eliminating the coupling between the high-level and low-level modules but doesn’t show how to implement it.

Dependency Inversion Principle - The limitation of DIP

This is where the Inversion Of Control (IOC) comes into the picture. IOC reveals the way to define the abstraction between the modules to reduce the coupling. In short, IOC defines the way to implement the Dependency Inversion Principle (DIP).

We will see IOC in more detail in the next article. Meanwhile, you can find a few more examples where you can apply the DIP.

Summing Up

The dependency Inversion Principle (DIP) suggests how to set the dependency between the modules. However, it doesn’t reveal the actual implementation details. In short, DIP can 

  • Break the whole system into independent modules with a high cohesion design.
  • Identify the modules in a high-level and low-level category.
  • Design the dependency in a way that a high-level module should not depend on low-level modules. 
  • Since DIP is a design principle, it only talks about what to do and not how to do it. For that, you need to rely on design patterns and methodologies.
  • In the next article, we will see Inversion of Principle (IOC), which shows a way to implement DIP – the How part.

Recommended For You

About the Author: Nilang

Nilang Patel is a technology evangelist who loves to spread knowledge and helping people in all possible ways. He is an author of two technical books - Java 9 Dependency and Spring 5.0 Projects.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.