1. Introduction

In this tutorial, we’ll learn about coupling in Java, including types and a description of each one of them. In the end, we briefly describe the Dependency Inversion principle and the Inversion of Control and how these are related to coupling.

2. Coupling in Java

When we talk about coupling, we describe the degree to which classes within our system depend on each other. Our goal during the development process is to reduce coupling.

Consider the following scenario. We’re designing a metadata collector application. This application collects metadata for us. It fetches metadata in XML format, then exports fetched metadata to a CSV file, and that’s all. An initial approach could be, as we can appreciate in the design:

Metadata collector diagrama tight coupling

Our module takes care of fetching, processing, and exporting the data. However, it’s a lousy design. This design infringes on the single responsibility principle. So, to improve our first design, we need to separate the concerns. After this change, our design looks like this:

Metadata collector diagrama separetion of concerns

Now, our design is decoupled into two new modules, XML Fetch and Export CSV. Meanwhile, the Metadata Collector Module depends on both. This design is better than our initial approach, but it’s still a work in progress. In the following sections, we’ll notice how we can improve our design based on good coupling practices.

3. Tight Coupling

When a group of classes is highly dependent on each other, or we’ve classes that assume a lot of responsibilities is called tight coupling. Another scenario is when an object creates another object for its usage. Tight coupling code is hard to maintain.

So, let us see this behavior using our base application. Let’s jump into some code definitions. First, our XMLFetch class:

public class XMLFetch {
    public List<Object> fetchMetadata() {
        List<Object> metadata = new ArrayList<>();
        // Do some stuff
        return metadata;
    }
}

Next, the CSVExport class:

public class CSVExport {
    public File export(List<Object> metadata) {
        System.out.println("Exporting data...");
        // Export Metadata
        File outputCSV = null;
        return outputCSV;
    }
}

And finally, our MetadataCollector class:

public class MetadataCollector {
    private XMLFetch xmlFetch = new XMLFetch();
    private CSVExport csvExport = new CSVExport();
    public void collectMetadata() {
        List<Object> metadata = xmlFetch.fetchMetadata();
        csvExport.export(metadata);
    }
}

As we can notice, our MetadataCollector class depends on XMLFecth and CSVExport classes. Additionally, it is in charge of creating them.

If we need to improve our collector, maybe add a new data JSON fetcher and export data in PDF format, we need to include these new elements inside our class. Let’s code the new “improved” class:

public class MetadataCollector {
    ...
    private CSVExport csvExport = new CSVExport();
    private PDFExport pdfExport = new PDFExport();
    public void collectMetadata(int inputType, int outputType) {
        if (outputType == 1) {
            List<Object> metadata = null;
            if (inputType == 1) {
                metadata = xmlFetch.fetchMetadata();
            } else {
                metadata = jsonFetch.fetchMetadata();
            }
            csvExport.export(metadata);
        } else {
            List<Object> metadata = null;
            if (inputType == 1) {
                metadata = xmlFetch.fetchMetadata();
            } else {
                metadata = jsonFetch.fetchMetadata();
            }
            pdfExport.export(metadata);
        }
    }
}

The Metadata Collector Module needs some flags to handle new functionalities. Based on the flag value, the respective child module will be instantiated. However, every new functionality not only makes our code more complex but also makes maintenance harder. This is a sign of tight coupling, and we must avoid it.

4. Loose Coupling

During the development process, the number of relationships among all our classes needs to be the fewest possible. This is called loose coupling. Loose coupling is when an object gets the object to be used from external sources. Our objects are independent one each other. Loosely coupled code reduces maintenance effort. Moreover, it provides much more flexibility to the system.

Loose coupling is expressed through the Dependency Inversion Principle. In the next section, we will describe it.

5. Dependency Inversion Principle

The Dependency Inversion Principle (DIP) refers to how high levels modules should not depend on low-level modules for their responsibilities. Both should depend on abstraction.

In our design, this is the fundamental problem. The metadata collector (high-level) module depends on fetch XML and export CSV data(low-level) modules.

But what can we do to improve our design? DIP shows us the solution, but it doesn’t talk about how to implement it. In this case, it is when Inversion of Control(IoC) takes action. IoC indicates the way of defining abstractions between our modules. To sum up, it is the way of implementing DIP.

So, let’s apply DIP and IoC to our current example. First, we need to define an interface for fetching and other for exporting data. Let’s jump into code to see how to do that:

public interface FetchMetadata {
    List<Object> fetchMetadata();
}

Simple, isn’t it? Now we define our exporting interface:

public interface ExportMetadata {
    File export(List<Object> metadata);
}

Moreover, we need to implement these interfaces in the correspondent class. In short, we need to update our current classes:

public class XMLFetch implements FetchMetadata {
    @Override
    public List<Object> fetchMetadata() {
        List<Object> metadata = new ArrayList<>();
        // Do some stuff
        return metadata;
    }
}

Next, we need to update the CSVExport class:

public class CSVExport implements ExportMetadata {
    @Override
    public File export(List<Object> metadata) {
        System.out.println("Exporting data...");
        // Export Metadata
        File outputCSV = null;
        return outputCSV;
    }
}

Additionally, we update the main module’s code to support new design changes. Let’s see how it looks:

public class MetadataCollector {
    private FetchMetadata fetchMetadata;
    private ExportMetadata exportMetadata;
    public MetadataCollector(FetchMetadata fetchMetadata, ExportMetadata exportMetadata) {
        this.fetchMetadata = fetchMetadata;
        this.exportMetadata = exportMetadata;
    }
    public void collectMetadata() {
        List<Object> metadata = fetchMetadata.fetchMetadata();
        exportMetadata.export(metadata);
    }
}

We can observe two main changes in our code. First, the class depends only on abstractions, not concrete types. On the other hand, we remove the dependency on low-level modules. There is no need to keep any logic related to low-level module creation in the collector module. The interaction with those modules is through a standard interface. The advantage of this design is that we can add new modules to fetch and export data, and our collector code won’t change.

By applying DIP and IoC, we improve our system design. By inverting (changing) the control, the application becomes decoupled, testable, extensible, and maintainable. The following image shows us how looks the current design:

Metadata collector diagrama using DIP and IoCFinally, we remove any tightly coupled code from our code base and enhance the initial design with loosely coupled code using DIP with IoC.

6. Conclusion

In this article, we covered coupling in Java. We first went through the general coupling definition. Then, we observed the differences between tightly coupling and loosely coupling. Later, we learned to apply DIP with IoC to get a loosely coupled code. This walk-through was done by following an example design. We could observe how our code improved in each step by applying good design patterns.

As usual, our code is available over on GitHub.