Inversion Of Control (IOC) – Build loosely coupled system

Inversion of Control is a design methodology that suggests inverting the flow of control from the main program to another framework or entity. This greatly helps in developing the loosely coupled system.

The term control refers to additional activities a program is performing apart from its main responsibility. For example, creating the instance of other dependent classes and maintaining them during the lifespan of the execution, handling the application flow, etc., can be considered a set of control flows a particular program has.

Generally, in a typical programming style, a single program manages multiple things altogether, which might sometimes not be directly related to it.

In the last article, we have seen how Dependency Inversion Principle (DIP) defines a clear guideline to divide the program into smaller, independent, and self-contained components called modules based on the responsibilities they carry.

If you wish to know more about DIP, I would recommend referring to the previous article. In that, we took the example of developing a Business Intelligent (BI) system. We started with the traditional approach of putting all the logic in a single logical unit. Then we divided them into multiple independent modules.

We also identified the high-level and low-level modules, but we stuck with updating high-level code for each new low-level module due to the tight coupling between the modules.

This was a fundamental problem in our design. As a solution, DIP suggests removing the dependency between modules but doesn’t talk about how to do that. We ultimately need to know how to arrange the high-level and low-level modules (the HOW part) to build a loosely coupled system.

Inversion of Control

Fortunately, the missing HOW part is fulfilled by IOC (Inversion of Control). IOC defines clear methodologies to invert (change) the flow of control, which eventually makes the system robust, extensible, maintainable, and testable.

In this article, we will take the same example code that we took in the previous article. With the IOC’s help, we will make the necessary arrangements to improve the overall design of an application and make it loosely coupled.

1. Inversion of Control (IOC) in a practical scenario

Let’s understand the concept of Inversion of Control (IOC) by looking at a real-life example. Suppose you are running a retail business and paying the tax on selling. The taxpaying process can be executed with the following scenarios.

1.1 Scenario -1 Direct interaction

Assume that you are running a local departmental store and you are doing all tax-related processes. You are directly dealing with the taxation department for various activities like filing returns, paying advance tax, get an input tax credit, and so on. The typical flow of paying the tax in this scenario would be as follows:

  • Starting off the taxpaying cycle, you pay the advance tax.
  • When you do purchase, you are paying input tax, so you are collecting all those invoices.
  • When you do sell, you pay tax to the government.
  • Then you can claim an input tax credit that you already paid while making the purchase, and so on.

In this scenario, the following diagram describes the whole process:Inversion of Control - tight coupling system

If your state/center government modifies the taxpaying procedure, you need to re-iterate the preceding steps based on the changes in those processes as follows:

  • New tax mechanisms such as changes in tax slabs, submitting the tax (online/offline), getting the input tax credit, and so on.
  • You might need to change the way of generating billing and invoices.

You need to adjust to the new changes yourself, and this requires a good amount of time to spend to make yourself familiar with.

In this case, the taxation department is controlling you. Meaning, you need to incorporate them into your business yourself for any changes initiated by the taxation department.

1.2 Scenario -2 Interaction through a mediator

In this scenario, instead of doing all those activities, you hire an agency that provides various accounting services to your firm.

Instead of the taxation department, you are interacting with the accounting agency for filing returns, paying tax, generating invoices, claiming the input tax credit, and so on. This scenario can be described in the following diagram:

Inversion of Control - Loosely Coupled system

You are just giving the trading details to the accounting agency. If there are any changes enforced by the government in taxation, it is the agency’s job to advise and implement the necessary steps to incorporate them. You probably do not need to reiterate the whole thing again.

You can continue to follow the same flow as earlier because the agency adjusted the changes, and you are not required to do anything much differently. In both these scenarios, you are dependent on the government taxation department to pay the tax, but still, scenario 2 is more straightforward in the following manner:

  • You do not need to check and verify if any changes happened in the taxation procedure. Accounting agencies will update you regarding the changes.
  • In case of changes, you do not need to consult with the taxation department. Instead, the agency will guide and apply the necessary changes in the accounting system.
  • Rather than closely depending on the taxation service, you can rely on the agency to perform various accounting activities more consistently.

In this case, the mediator (agency) takes care of the changes. You are just relying on simple and consistent interaction with the mediator. Now you are controlling the taxation department through an agency to accommodate the changes.

Instead of firmly relying on your dependencies, it’s always a good idea to delegate the control of managing them to someone else.

In scenario 2, we are just inverting (shifting) the control of managing the taxation changes from you to the accounting agency. In short, by inverting (changing) the control, your application becomes decoupled, testable, extensible, and maintainable.

2. Implementing DIP through IOC (Inversion of Control)

DIP suggests the high-level module should not depend on a low-level module; instead, both should depend on the abstraction. IOC provides a way to implement (the HOW part) DIP.

In other words, IOC actually helps in defining abstraction between high-level and low-level modules. It’s time to apply the IOC concept to Business Intelligence (BI) system – a sample application that we have created in the previous article.

The application design’s fundamental problem was the tight coupling between the high-level (Data Analysis) module and low-level (Import and Export) modules. Our next goal is to break this dependency. To achieve this, IOC suggests inverting control. The process of inverting the flow of control can be achieved in the following three ways.

  • Inverting the interface: The high-level module defines the interface, and low-level modules have to follow it.
  • Inverting object creation: Change the dependency object’s creation from your main module to some other program or framework.
  • Inverting flow: Change the flow of the application.

We will see each of the ways in detail as follows.

2.1 Inverting the Interface

Changing the control of interaction from low-level modules to high-level modules is called inverting the interface. Ideally, the high-level module must decide which low-level modules can interact instead of updating its code on each new low-level module integration.

In short, a high-level module will manage the flow of control by defining an interface, which must be followed by all low-level modules which need to interact with the high-level module. This can be described with the following diagram:

Achieve IOC through Inverting the Interface

The Data analysis (high-level) module deals with Import data and Export data (low-level) modules through a well-defined interface. This design can accommodate a few more Import and Export data modules without affecting the data analysis module’s code.

As long as low-level modules comply with the interface, the high-level module is happy to interact with them.

The high-level module is no more dependent on low-level modules, and both interact through a common interface. Also the concrete implementations (low-level modules’ class) depend on abstraction(interface). This is how DIP is implemented by inverting the interface.

Let’s apply the new design to our code. To define the abstraction, we need to create the interface that corresponds to each low-level module as follows.

public interface ExportData {
    // Common method implemented by all low level modules (exporters)
    public File exportData(List<Object[]> dataLst);
}

public interface ImportData {
    // Common method implemented by all low level modules (importers)
    public List<Object[]> importData();
}

Next is to update all low-level classes to implement these interfaces as follows.

public class ImportDBData implements ImportData {
    public List<Object[]> importData() {
        List<Object[]> dataFromDB = new ArrayList<Object[]>();
        System.out.println(" Importing data from database ...");
        return dataFromDB;
    }
}

public class ImportWebServiceData implements ImportData {
    public List<Object[]> importData() {
        List<Object[]> dataFromWebService = new ArrayList<Object[]>();
        System.out.println(" Importing data from webservice...");
        return dataFromWebService;
    }
}

public class ExportExcel implements ExportData {
    public File exportData(List<Object[]> dataLst) {
        File outputExcel = null;
        System.out.println(" Exporting data to Excel ...");
        return outputExcel;
    }
}

public class ExportHTML implements ExportData {
    public File exportData(List<Object[]> dataLst) {
        File outputHTML = null;
        System.out.println(" Exporting data to HTML ...");
        return outputHTML;
    }
}

Our high-level module (Data analysis) will use these interfaces to interact with the low-level modules. The change in the high-level module would be as follows:

public class DataAnalysis {

    private ExportData exportData = null;
    private ImportData importData = null;

    public void processDataAnalysis(int inputMethod, int outputMethod) {

        List<Object[]> dataLst = null;
        if (outputMethod == 1) {
            if (inputMethod == 1) {
                importData = new ImportDBData();
                dataLst = importData.importData();
            } else {
                importData = new ImportWebServiceData();
                dataLst = importData.importData();
            }
            exportData = new ExportHTML();
            exportData.exportData(dataLst);
        } else if (outputMethod == 2) {
            if (inputMethod == 1) {
                importData = new ImportDBData();
                dataLst = importData.importData();
            } else {
                importData = new ImportWebServiceData();
                dataLst = importData.importData();
            }
            exportData = new ExportExcel();
            exportData.exportData(dataLst);
        }
    }
}

Quick observation: The processDataAnalysis method is more flexible in allowing the integration of additional import and export modules. The mechanism of Inverting the Interface makes this possible. This looks perfect. But hold on.

Still, there is a problem with the above design. The DataAnalysis (high level) class is still responsible for creating low-level (importer and exporters) modules’ instances.

In other words, the object creation dependency is still with a high-level module. Due to this, 100% decoupling is not achieved between high and low-level modules, even after inverting the interface. We have to keep adding if/else block based on a flag for additional low-level module integration. This way, the high-level module’s code keeps changing.

The clear solution to this situation is to invert object creation from the high-level module to some other program or framework. This is the second step for achieving IOC.

2.2. Inverting Object creation

After setting up the abstraction between the modules, the next step is to shift the object creation logic outside of the high-level module. Why is it so important? Let’s see with one more real-life example. Assume you are developing a Racing game. The player can choose various vehicles based on the level and point he earned.

In this application, the Player is a high-level module, whereas vehicles are low-level modules. First, we will define an abstraction by inverting the interface.

All vehicles (low-level modules) implement the Vehicle interface to interact with Player (high-level modules). In the first version, you kept 3 vehicles in the racing game. If the vehicle creation logic is within the Player module, It looks as follows:

public class Player {
    private Vehicle vehicle;

    public void selectVehicle(int vehicleId) {
        switch (vehicleId) {
        case 1:
            vehicle = new Scooter();
            break;
        case 2:
            vehicle = new Bike();
            break;
        case 3:
            vehicle = new Car();
            break;
        }
    }

    public void startRace() {
        if (this.vehicle != null) {
            vehicle.run();
        }
    }
}

Every time you add new vehicles to your game in future releases, the Player class code keeps changing. To avoid this problem, we need to make sure the object creation logic should be outside of the Player class as follows:

public class Player {
    private Vehicle vehicle;

    public void selectVehicle(Vehicle vehicleFromOtherFramework) {
        this.vehicle = vehicleFromOtherFramework;
    }

    public void startRace() {
        if (this.vehicle != null) {
            vehicle.run();
        }
    }
}

Quick observation:

  • We are passing the object of the Vehicle inside the Player class through the interface. 
  • A Player is no longer holding responsibility for creating the object of low-level modules.
  • Both modules are interacting through the interface defined by a high-level module.
  • Player (a high-level module) can add additional Vehicles (low-level modules) without modifying its code.

This is how inverting the object creation helps in achieving modularity. Let’s apply this solution to our BI system. The updated code looks as follows:

public class DataAnalysis {
	private ExportData exportData = null;
	private ImportData importData = null;

	public void setExportData(ExportData exportData) {
		this.exportData = exportData;
	}
	public void setImportData(ImportData importData) {
		this.importData = importData;
	}
	public void processDataAnalysis(int inputMethod, int outputMethod){
		List<Object[]> dataLst = importData.importData();
		exportData.exportData(dataLst);
	}
}

The code not only looks cleaner but compact as well. Quick observation as follows:

  • Each of the Objects of type ImportData and ExportData (low-level modules) are created outside of the DataAnalysis class(high-level module).
  • Modules are 100% decoupled now.
  • DataAnalysis module can accommodate additional ExportData and ImportData modules without changing its code.

2.3 Changing the flow (Flow inversion)

This is another way of achieving IOC. Let’s try to understand it by taking a simple example. Remember the old days, when most of the software was running without GUI. It was the initial days of the software evolution.

Such programs were running as command line executors, taking information one by another.

For example, a command-line code taking your information to create a user account. First, it will ask the name. You have to give the value, and then it will ask for email and so on. In short, this application’s flow is procedural, where you need to give information linearly – step by step.

Here the flow of the application is controlling you as a user. If we want to invert it, we need to take control of giving input to us. This can be achieved by providing a simple GUI screen to provide all the information in one go. The user is free to add the information in any sequence he wants.

In short, there is no need to maintain the flow. When a user clicks on the Save button, the system will send all the information in one go. This is how inverting the application flow helps in achieving the Inversion of Control (IOC).

3. Design Patterns to support the Inversion of Control (IOC)

At this point, It’s clear that IOC is a way to achieve a DIP (Dependency Inversion Principle). However, it’s still not clear who will provide the dependencies to the high-level module.

Relationship between DIP, IOC and various design patterns like DI, Factory, Service locator etc.

Certain design patterns help in this scenario. They will create and provide the low-level objects (dependencies) to a high-level module. Below is a list of design patterns that do this job.

  1. Dependency Injection
  2. Factory 
  3. Service Locator
  4. Observer
  5. Template method

We will see how each of these design patterns helps manage the dependency and allow achieving Inversion of Control in separate blogs.

4. Summary

Inversion of Control (IOC) is a design methodology to implement the DIP (Dependency Inversion Principle). Inverting control refers to the process of removing the dependency. Following are the ways to achieve Inversion of Control (IOC)

  • Interface inversion.
  • Object creation inversion.
  • Flow inversion.

The new architecture with the Inversion of Control mechanism brings the following advantages.

  • Decoupling – especially between the execution logic and its implementation.
  • Accommodation – Addition low-level modules (implementation) can be easily plugged and play.
  • Modularity – the system can be designed in small independent modules.
  • Testing – ease of mocking the dependency results in smooth testing.

IOC helps in decoupling the modules. However, it doesn’t talk about how to supply the dependencies. This is where the design patterns come into the picture. Certain design patterns help in creating dependency (low-level) objects. You can think of more practical scenarios to apply the IOC methodology. This will help you to understand the core concept in more detail.

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.