Design Principles Series – Coupling

Design complexity can be identified using Coupling & Cohesion. In this article, we will discuss about Coupling.

It is important to keep system design as simple as possible and minimizing complexity is a common principle in software engineering.

When the design complexity increases, it can lead to several challenges and issues.

  • Difficulty in Maintaining Consistency: Complexity brings unexpected bugs in unrelated functionality of the application (when a code changed in a functionality of the application, another unrelated functionality can break).
  • Reduced Understandability & Increased Bug Occurrence: Developers will struggle to visualize the complex designs and it is difficult to comprehend. This causes misunderstanding and misinformation of the understanding in functionality which results in wrong behavior of the system and causes more bugs. Due to the increased complexity, it is harder to identify and fix the bugs.

How to know your design is less complex? How to evaluate the Design Complexity of your system?

Design complexity can be identified using Coupling & Cohesion. In this article, we will discuss about Coupling.

Coupling:

Coupling refers the connections between the modules or components. It measures how closely the modules/components are connected.

When a component depends highly on other component, it is considered as tight coupling.

When we say high dependency, it means that,

  • Difficulty in testing individual components as one component need another component while testing.
  • The system becomes fragile: A change or modification in code or in functionality will break the system’s main behavior.
  • Hard to reuse the code/functionality.
  • And more importantly, difficulty in extending the system.

So, having tight coupling is not a good design.

Tight Coupling:

When components highly depend on other components, then tight coupling can occur.

Let’s see a couple of situations where tight coupling can occur.

Problems with Global Variables or Constants:

When a variable is global and shared by various components, then that means the components are dependent on each other. The global variable can be meaningful to one component but may not be the same for all other components. The another problem is testing becomes hard for the methods which use global variables.

public class PriceCanculator {
    public void getPriceWithTax(Product product) {
        // Get tax rate from global variables or constants
        double taxRate = TaxRate.GST_TAX_RATE;
        .....
    }
}

In the above example, we used Constants for getting tax rate value.

However the tax rate can differ based on the type of the products ordered. In that case, this constant can become meaningless for other parts of the project.

As a solution to this problem, the methods can get tax rate as a parameter based on which product is ordered.

Problems with Direct Object Instantiation:

In the below example, the LibraryData class uses the method displayDetails() from Book class. And it directly creates the instance of Book and calls this method directly.

public class LibraryData {
    private Book book;
    
    public LibraryData() {
		// Creating a specific instance of Book inside the Bookstore class
        book = new Book(); 
    }
    
    public void printBookDetails() {
        // Directly accessing the Book class's method
		book.printDetails(); 
    }
}

public class Book {
    public void printDetails() {
        // Code to print book details
    }
}

In future the library would like to print details based on the type of the book for example, printing the text book with the details of the grade which can use that book and for fictions displaying a brief story or one-liner for the customers to understand the genre of the book etc. For every change or enhancement in the Book class, the LibraryData class needs to be changed. This situation indicates tight coupling.

Loose Coupling:

Dependency Injection instead of Direct Object instantiation:

In the below example, the Book class is converted to an interface and it is implemented with various types of books.

  1. When we separate abstraction from the implementation, the components can interact though the abstractions instead of concrete implementations. Thus the components will only rely on the interfaces and the interfaces does not need to be changed when new implementation for that interface occurs in future.
  2. Instead of creating the instance directly, we can send the type of book as an argument through the constructor parameter. This way, the diplayDetails method can be used for different purpose without changing the implementation in LibraryData class.

The above changes will reduce the dependency between LibraryData component and the Book.

public class LibraryData {
    private Book book;
    
    public LibraryData(Book book) {
        // Get the Book instance using dependency injection
        this.book = book; 
    }
    
    public void displayBookDetails() {
        book.displayDetails(); 
    }
}

public interface Book {
    void displayDetails();
}

public class NonFiction implements Book {
    public void displayDetails() {
        // Code to display nonfiction details
    }
}

public class TextBook implements Book {
    public void displayDetails() {
        // Code to display text book details include which grade children can use it
    }
}

public class Fiction implements Book {
    public void displayDetails() {
        // Code to display fiction book details including a brief story line 
    }
}

public class NonFiction implements Book {
    public void displayDetails() {
        // Code to display non-fiction book details with the facts which author describes
    }
}

public class Main {
    public static void main(String[] args) {
        // Instance of a Book with Fiction
        Book book = new Fiction(); // or new Textbook()

        // Create instance of libraryData and pass the Book instance
        LibraryData libraryData = new LibraryData(book);

        // Call the displayBookDetails() method
        // This method will execute the displayDetails() function from Fiction class
        libraryData.displayDetails();
    }
}

Notice that, in the main method, we create an instance for Fiction and send it to LibraryData.

Now the libraryData class will use the Fiction class’s implementations. This way the main method is not relying on the Book class (concrete implementation) and it only relies on the interface LibraryData which is an abstraction.


Conclusion:

In this article we learned how to avoid tight coupling and to achieve loose coupling using java programs.

Lets talk about cohesion in next article.

Thank you and Happy Programming!


Asha Ponraj
Asha Ponraj

Data science and Machine Learning enthusiast | Software Developer | Blog Writter

Articles: 86

Leave a Reply

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