Have you ever found yourself navigating multiple files to determine how a straightforward function behaves? On the other hand, have you ever been in the middle of a good class that does everything while you are struggling to add your minor improvement? If you have been here before, maybe the code you were exploring is tightly coupled and low in cohesion.
Thus, what exactly is tightly coupled and low in cohesion code? Why is tightly coupled and low in cohesion code bad? And how can you write better code?
To answer this question, let’s have a look at coupling and cohesion.
Coupling
In object oriented design, coupling refers to the degree of direct knowledge that one element has of another. In other words, how often do changes in class A force related changes in class B.
Let’s take a look at this code snippet:
public class Order {
private CashPayment payment = new CashPayment();
public void processPayment() {
payment.process();
}
}
public class CashPayment {
public void process() {
// Process logic
}
}
The Order class depends directly on the CashPayment class, which makes it difficult to modify the CashPayment class without affecting the Order class. In this case, we say Order class is tightly coupled with CashPayment class.
To make the code loosely coupled, we can introduce abstractions to the CashPayment class. Here is an updated version of the code:
public class Order {
private PaymentType payment;
public Order(PaymentType paymentType) {
payment = paymentType;
}
public void processPayment() {
payment.process();
}
}
public interface PaymentType {
public void process();
}
public class CashPayment implements PaymentType {
public void process() {
// Process logic
}
}
In the updated code, we introduced the PaymentType interface, which defines the process() method. The Order class now depends on the PaymentType interface instead of the concrete CashPayment class. By using an interface, we decouple the Order class from the specific implementation of the CashPayment class.
This change provides better flexibility. It also allows the Order class to work with any class that implements the PaymentType interface. In our case, we can easily add CreditCardPayement class without modifying the Order class.
To summarize, good code is loosely coupled because modifications will be made in the associated class.
Cohesion
In object-oriented design, cohesion refers to how a single class is designed. Cohesion is closely associated with making sure that a class is designed with a single, well-focused purpose. In other words, if the stuff that are grouped in a class tends to be similar in many aspects, then the class is said to have high cohesion.
Let’s take a look at this code snippet:
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getArea() {
return 3.14 * radius * radius;
}
public double getPerimeter() {
return 2 * 3.14 * radius;
}
public String toString() {
return "Circle (radius: " + radius + ")";
}
public void printDetails() {
System.out.println("Shape: " + this.toString());
System.out.println("Area: " + getArea());
System.out.println("Perimeter: " + getPerimeter());
}
}
The Circle class has an attribute radius, as well as methods for calculating the area, perimeter, and printing the details of the circle. However, the printDetails() method violates the principle of single responsibility by combining outputting details with calculating area and perimeter. We say our code is low in cohesion.
To improve cohesion, we can separate the responsibilities by creating a new class just for displaying the details of the circle:
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getArea() {
return 3.14 * radius * radius;
}
public double getPerimeter() {
return 2 * 3.14 * radius;
}
public String toString() {
return "Circle (radius: " + radius + ")";
}
}
public class CircleDetailsPrinter {
private Circle circle;
public CircleDetailsPrinter(Circle circle) {
this.circle = circle;
}
public void printDetails() {
System.out.println("Shape: " + circle.toString());
System.out.println("Area: " + circle.getArea());
System.out.println("Perimeter: " + circle.getPerimeter());
}
}
In the updated code, the CircleDetailsPrinter class is introduced to handle the printing of circle details. The Circle class now delegates the responsibility of printing to the CircleDetailsPrinter class.
By separating the printing functionality into its own class, we say that our code is highly cohesive. Each class has a single responsibility, which makes the code clearer and easier to maintain.
Note that in this example, cohesion is improved by separating the responsibilities between two classes. However, it’s important to establish a balance and avoid excessive class separation. The goal is to have focused classes with clear responsibilities without unnecessarily splitting them into too many small units.
Good code is highly cohesive because modifications will be made in the associated class.
Now that we understand the concept of cohesion, let’s apply what we have learned about coupling to the last example. We see that the CircleDetailsPrinter class is tightly coupled with the Circle class. Let’s decouple the CircleDetailsPrinter class from the specific implementation of the Circle class:
public interface Shape {
public double getArea();
public double getPerimeter();
}
public class Circle implements Shape {
// ...
}
public class Square implements Shape {
// ...
}
public class Rectangle implements Shape {
// ...
}
public class ShapeDetailsPrinter {
private Shape shape;
public ShapeDetailsPrinter(Shape shape) {
this.shape = shape;
}
public void printDetails() {
System.out.println("Shape: " + shape.toString());
System.out.println("Area: " + shape.getArea());
System.out.println("Perimeter: " + shape.getPerimeter());
}
}
By introducing the Shape interface, we decoupled the CircleDetailsPrinter class from the specific implementation of the Circle class. Additionally, we leverage the new ShapeDetailsPrinter class to serve the Square and Rectangle classes without duplicating code.
Conclusion
In conclusion, loosely coupled and high-cohesion code is the best. Additionally, we learned how hard it is to maintain tightly coupled and low-cohesion code and how to rework it.
I hope you found this article insightful. Thank you for reading.
Please make sure to follow me to get the updates.
You can also find me elsewhere on the internet.
Coupling and Cohesion in Object-Oriented Programming was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.