Cohesion and Coupling in Object Oriented Programming (OOPS)

As software developers, we may have heard of the concepts of Cohesion and Coupling in Object-Oriented Programming. These two concepts are used to measure our code quality and ensure it is maintainable and scalable. Let's understand the meaning of both terms.

  • Cohesion defines how effectively elements within a module or object work together to fulfil a single well-defined purpose. A high level of cohesion means that elements within a module are tightly integrated and working together to achieve the desired functionality.
  • Coupling defines the degree of interdependence between software modules. Tight coupling means that modules are closely connected and changes in one module can affect others. On the other hand, loose coupling means that modules are independent and changes in one module have minimal impact on others.

coupling vs cohesion in oops

So, it’s best practice in OOPS to achieve loose coupling and high cohesion because it helps us to create classes or modules that are more flexible and less prone to break when changes are made. 

Let’s move forward to understand the idea of low and high cohesion with an example. After this section, we will also discuss the idea of tight and loose coupling.

What is the meaning of Low Cohesion?

In low cohesion, a module or object within a system has multiple responsibilities that are not closely related i.e. a single module or object performs a diverse range of independent tasks. As a result:

  • This makes our code difficult to understand.
  • This can lead to confusion and make it challenging to modify or update the code.

For example, let’s consider a class “StudentRecord” that has following responsibilities: 1) Maintaining student information 2) Calculating student grades 3) Printing student transcripts 4) Sending email notifications to students.

Here is an example java code:

public class StudentRecord {
    private String name;
    private String id;
    private String address;
    private double[] grades;

    public StudentRecord(String name, String id, String address, double[] grades) {
        this.name = name;
        this.id = id;
        this.address = address;
        this.grades = grades;
    }

    public double calculateAverageGrade() {
        double sum = 0;
        for (double grade : grades) {
            sum += grade;
        }
        return sum / grades.length;
    }

    public void printTranscript() {
        System.out.println("Student Transcript");
        System.out.println("Name: " + name);
        System.out.println("ID: " + id);
        System.out.println("Address: " + address);
        System.out.println("Average Grade: " + calculateAverageGrade());
    }

    public void sendEmailNotification(String emailAddress) {
        // code to send email notification
    }
}

In this case, StudentRecord class has multiple unrelated responsibilities like maintaining student information, calculating grades, printing transcripts, and sending emails. These all are distinct tasks that could be handled by separate classes or modules. So this code becomes more difficult to understand and maintain. How? Let’s think!

  • Suppose we want to add a new field to the student data. This may affect or break the code that calculates average grade or sends email notifications or prints the transcript. If there are a lot of other unrelated responsibilities, then we need to do a lot of work to identify breaking changes. Idea is simple: If a class has multiple reasons to change then it has multiple reasons to break as well.
  • If we want to use StudentRecord class in a different context, we would need to copy entire class, including all of its responsibilities.

How can make this code more cohesive? Let’s solve this problem by understanding the idea of high cohesion.

What is the meaning of High Cohesion?

In OOPS, high cohesion is an idea where each module or component within a system has a single, well-defined responsibility. In other words, each module or component is highly focused on a specific task and has all the necessary information and resources to perform that task effectively.

So to make the code more cohesive, we can use the idea of Single Responsibility Principle, which states that a class should have only one reason to change. In other words, we should refactor the code so that each responsibility is encapsulated in a separate class.

Based on this idea, let’s solve the problem of low cohesion in previous example. We can divide the responsibilities of the original “StudentRecord” class into four separate classes: Student, GradeCalculator, TranscriptPrinter, and EmailNotifier.

public class Student {
    private String name;
    private String id;
    private String address;
    private double[] grades;

    public Student(String name, String id, String address, double[] grades) {
        this.name = name;
        this.id = id;
        this.address = address;
        this.grades = grades;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public String getAddress() {
        return address;
    }
    
    public double[] getGrades() {
        return grades;
    }
}

public class GradeCalculator {
    public static double calculateAverage(Student student) {
        double sum = 0;
        double[] grades = student.getGrades();
        for (double grade : grades) {
            sum += grade;
        }
        return sum / grades.length;
    }
}

public class TranscriptPrinter {
    public static void print(Student student) {
        double[] grades = student.getGrades();
        System.out.println("Student Transcript");
        System.out.println("Name: " + student.getName());
        System.out.println("ID: " + student.getId());
        System.out.println("Address: " + student.getAddress());
        System.out.println("Average Grade: " + GradeCalculator.calculateAverage(student));
    }
}

public class EmailNotifier {
    public static void sendNotification(String emailAddress, Student student) {
        // code to send email notification
    }
}

Now, this separation of concerns makes the code easier to understand, reuse and maintain, as each class has a well-defined purpose and responsibility. This also helps us to find and fix bugs easily, and understand how different components work together. What are the other advantages? Think and explore!

What is the meaning of Tight coupling?

In OOPS, tight coupling is a situation when classes or modules have many dependencies on each other.

  • Tight coupling makes it difficult to modify or extend our modules without affecting the other.
  • This can lead to an increased risk of bugs and unintended side effects.

Here is an example of tight coupling in Java:

class Car {
    private Engine engine;
  
    public Car() {
        this.engine = new Engine();
    }
  
    public void start() {
        engine.start();
    }
  
    public void stop() {
        engine.stop();
    }
}

class Engine {
    public void start() {
        // Some implementation
    }
  
    public void stop() {
        // Some implementation
    }
}

In this example, Car class has a direct reference to the Engine class, and it also calls start() and stop() methods on it. This creates a tight coupling because if we want to modify or extend Engine class, we also need to consider how it will affect Car class. For example:

  • If we change the method signatures of start or stop methods in Engine class, we also need to change corresponding method calls in Car class.
  • Suppose, we want to add specific implementations of Engine like ElectricEngine or a HybridEngine, then things get more complex further!

How can make this code less coupled? Let’s solve this problem by understanding the idea of loose coupling.

What is the meaning of Loose Coupling?

In OOPS, loose coupling is a situation when classes or modules have minimal dependencies on each other i.e. changes in one class or module are unlikely to affect the other. 

  • This makes our code modular and reduces the risk of introducing bugs.
  • This makes our code easier to maintain and extend over time.

To avoid tight coupling, we can use design patterns like Dependency Injection or use interfaces to decouple dependencies between classes.

For example, to solve the problem of tight coupling in above code, we can add the Engine interface and create a ConcreteEngine class that implements the Engine interface. Here Engine interface defines methods that Car class needs to use, but it doesn’t specify exactly how these methods are implemented.

interface Engine {
    void start();
    void stop();
}

class ConcreteEngine implements Engine {
    public void start() {
        // Some implementation
    }
  
    public void stop() {
        // Some implementation
    }
}

class Car {
    private Engine engine;
  
    public Car(Engine engine) {
        this.engine = engine;
    }
  
    public void start() {
        engine.start();
    }
  
    public void stop() {
        engine.stop();
    }
}

This will help us to create multiple concrete implementations of the Engine interface, each with its own unique behavior. For example, we can also create additional implementations of the Engine interface, such as ElectricEngine class or a HybridEngine class.

  • Now Car class depends on the Engine interface, not on a specific implementation of the Engine. When we create an instance of the Car class, we pass in an instance of the Engine interface. This can be an instance of any implementation like ConcreteEngine, ElectricEngine, or HybridEngine.
  • This also helps us to create different Car instances with different types of engines, without having to modify the Car class itself.

By decoupling Car and Engine classes, we can make each class easier to modify or extend without affecting the other. For example, if we want to add new methods to the Engine interface, we can do so without having to modify the Car class. If we want to change the behavior of the ConcreteEngine class, we can do so without having to modify the Car class.

Types of Cohesion in OOPS

  • Functional cohesion: Performing a single, well-defined task without any unintended consequences. This type of cohesion is often seen in well-designed methods. (Highly desirable)
  • Informational cohesion: Representing a set of data and a group of operations that can be performed on that data. This form of cohesion is commonly observed in well-designed classes.
  • Procedural cohesion: Series of tasks that must be executed in a specific order.
  • Temporal cohesion: Set of tasks that must be performed around the same time, such as during initialization or cleanup.
  • Logical cohesion: Among related set of tasks, caller selects which task to perform in each case.
  • Coincidental cohesion: Set of tasks with no logical connection and they are only grouped together for convenience or by chance. (Less desirable)

Types of Coupling in OOPS

  • Content coupling: When a module modifies or relies on the internal details of another module.
  • Control coupling: When one module controls the flow of control in another module.
  • Data coupling: When one module passes data to another module without knowing its internal structure.
  • Stamp coupling: When one module uses only a part of the other module’s state.
  • Common coupling: When multiple modules share a common data structure.
  • External coupling: When a module depends on external resources, such as files or databases.
  • Message coupling: When modules communicate with each other through messages.

Best practices to achieve High Cohesion in OOPS

  • Each class should have a single responsibility and all its methods and data should be related to that responsibility.
  • We should encapsulate data and methods that belong together within the same class. This makes the class easier to maintain and understand.
  • We should aim to design system as a collection of small, reusable modules, each with its own high cohesion. This makes it easier to identify and fix any problems, as well as to extend and reuse the code.
  • We should identify and use proper design patterns to maintain high cohesion.

How to Check Cohesion?

To check the cohesion of a class that you have created, you can follow these steps:

  1. Identify the responsibilities of the class. What is the main purpose of the class, and what tasks does it perform?
  2. Analyze the methods in the class. Do they all contribute to the same purpose, or do they perform unrelated tasks?
  3. Check if the methods in the class use the same instance variables or share common functionality. Do they rely on each other to complete their tasks?
  4. Determine if there is any duplicated code, or if methods perform similar functions but with different parameters.
  5. Finally, evaluate if the class has a clear and single responsibility.

If the methods in the class are closely related to the same responsibility or purpose, use the same instance variables, share common functionality, and there is no duplicated code or irrelevant methods, then the class has high cohesion.

If the methods in the class are unrelated to each other, have different instance variables, and do not share common functionality, then the class has low cohesion.

Overall, checking cohesion requires analyzing the structure of the class and identifying whether its methods and variables work together towards the same goal or perform unrelated tasks. This helps in designing classes that are more maintainable, testable, and easy to understand.

Best practices to achieve Loose Coupling in OOPS

  • By defining interfaces, we can ensure that objects communicate with each other in a well-defined manner, rather than having direct references to concrete implementations.
  • By using dependency injection, we can create objects with necessary dependencies they require to function, rather than creating objects with their own dependencies.
  • We can use design patterns, such as facade or adapter pattern to provide a unified interface for interacting with multiple objects. This helps to further decouple objects and make the system more modular.
  • We should use encapsulation to limits the visibility of data and methods, and avoids exposing implementation details. By doing so, we reduce number of dependencies between objects and make it easier to modify or extend the code without affecting other parts of the system.

How to Check Coupling?

For coupling, it is important to consider the level of interdependence between two modules. To check coupling in an OOP application, you can try the following:

  1. Look at the classes used by a particular class. If the class is dependent on many other classes, this may indicate a high degree of coupling.
  2. Count the number of method calls made between classes. A large number of method calls can indicate a high degree of coupling.
  3. Determine the level of abstraction at which classes are interacting. If classes are interacting at a low level of abstraction, there may be a high degree of coupling.

Advantages of High Cohesion in OOPS

Let’s take an example of a house. Each room in the house has a specific purpose and all furniture, fixtures, and decorations in that room are related to that purpose. This makes it easy for us to understand what each room is for and what we can find in those rooms. It also makes it easier for us to maintain and fix things if something breaks, as we know exactly what belongs in that room.

Similarly, when we write code with high cohesion, each class or module is like a room in the house. It has a single, well-defined responsibility, and all its methods and data are related to that responsibility. This makes it easy for other developers to understand what the class is for, what it does, and how it works. It also makes it easier for them to maintain and fix the code if they need to, as they can easily identify the source of any problems.

Advantages of Loose Coupling in OOPS

Let’s again take the example of building a house. Just like a software system, a house is made up of different parts that need to work together. When these parts are connected in a loosely coupled way, it’s easier to make changes or additions to one part without affecting the others.

For example, if we want to add a new room to our house, we don’t want to tear down the entire house. Similarly, in software, if we want to add a new feature to our code, we don’t want to rewrite the entire system. So loose coupling allows us to make changes to one part of the code without affecting the rest, just like adding a room to our house.

Finally, low coupling also reduces the risk of unintended side effects. If each room in a house is connected in a clear and well-defined way, it’s easier to understand how they all work together. In software, low coupling makes it easier to understand the relationships between objects, reducing the risk of bugs and making it easier to maintain over time.

Conclusion

Cohesion and coupling are interdependent concepts in Object-Oriented Programming (OOP). The level of one can impact the level of the other. High cohesion is often accompanied by loose coupling. When the elements within a module are tightly related to each other and serve a single, well-defined purpose, they will have limited interaction and dependence on other modules. This results in a loose coupling between the module and other modules.

On the other hand, tight coupling can be an indicator of low cohesion. When elements from two modules are heavily dependent on each other, it suggests that the elements are spread across both modules and lack a clear, well-defined purpose. This results in low cohesion between the modules.

So in a well-designed OOP system, we should aim to achieve high cohesion and low coupling. This will help us to enhance the overall maintainability, scalability, and readability of the code. Enjoy learning, Enjoy OOPS!

Share Your Insights

More from EnjoyAlgorithms

Self-paced Courses and Blogs

Coding Interview

Machine Learning

System Design

Our Newsletter

Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.