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.
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.
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:
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!
How can make this code more cohesive? Let’s solve this problem by understanding the idea 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!
In OOPS, tight coupling is a situation when classes or modules have many dependencies on each other.
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:
How can make this code less coupled? Let’s solve this problem by understanding the idea 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.
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.
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.
To check the cohesion of a class that you have created, you can follow these steps:
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.
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:
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.
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.
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!