Coupling and Cohesion Explained with Java Examples
Introduction
This tutorial explains coupling and cohesion, the two measures of design quality that most other design principles are built on. We will start with what each term means and why the goal is high cohesion with low coupling. We will then look at the different kinds of cohesion and coupling, from best to worst. Next, we will refactor a Java class with low cohesion, and a Java example with tight coupling, and see the difference in behaviour. Lastly, we will see how to spot and measure both in a codebase.What is cohesion
Cohesion is how closely the parts inside one module belong together. A module can be a method, a class, a package or a service. In a highly cohesive class, the fields and methods all work towards one purpose; in a class with low cohesion, they do unrelated things that happen to share a file.A class named
EmployeeUtils that calculates tax, validates email addresses and formats dates has low cohesion. A class named TaxCalculator whose every method is about tax has high cohesion. Cohesion is the idea behind the Single Responsibility PrincipleClick to read: Single Responsibility Principle.
What is coupling
Coupling is how much one module depends on other modules. Two classes are tightly coupled when a change in one forces a change in the other, or when one cannot be used, compiled or tested without the other. They are loosely coupled when they interact through a small, stable interface and know little about each other's internals.Coupling cannot be zero - modules that never interact do not form a program. The aim is fewer connections, and connections that are narrow and explicit.
The goal - high cohesion, low coupling
The two measures are linked. When related code is grouped together (high cohesion), most interactions happen inside a module, so fewer have to cross module boundaries (low coupling). When related code is scattered (low cohesion), every change touches several modules, and those modules need many connections to each other.
Kinds of cohesion and coupling
Larry Constantine and his co-authors classified both in the 1970s, in the work that became Structured Design. The names are still useful because they tell you why a design is weak and what to change. The diagram lists them from best to worst.
- Coincidental cohesion: a
UtilsorHelperclass collecting unrelated methods. - Logical cohesion: one method doing different things depending on a parameter, such as
process(type, data)with aswitchinside. - Common coupling: classes reading and writing the same
public staticmutable field. - Control coupling: passing a
booleanorenumflag that tells the method which of its behaviours to run. - Stamp coupling: passing a whole object when the method uses only one field, which ties the method to that object's type.
- Content coupling: reaching into another object's internals, such as modifying a list returned by its getter.
Example 1 - improving cohesion
Here is a typical low-cohesion class. Its three methods have nothing in common except that they involve employees.An EmployeeUtils class with coincidental cohesion
public class EmployeeUtils {
public static double incomeTax(double annualSalary) { ... } // payroll rules
public static boolean isValidEmail(String email) { ... } // input validation
public static String formatJoiningDate(LocalDate date) { ... } // presentation
}
isValidEmail() now depends on a class that also contains tax rules. Splitting it by purpose gives three cohesive types -
The same methods regrouped into cohesive classes
public final class IncomeTax { // everything about tax
public static double forAnnualSalary(double salary) { ... }
public static double marginalRate(double salary) { ... }
}
public record EmailAddress(String value) { // validation lives with the data
public EmailAddress {
if (!value.matches("[^@\\s]+@[^@\\s]+\\.[^@\\s]+"))
throw new IllegalArgumentException("Invalid email: " + value);
}
}
public final class EmployeeFormats { // presentation only
public static String joiningDate(LocalDate date) { ... }
}
EmailAddress is not just a renamed helper. Moving the validation into the type that holds the data means an invalid email address cannot exist anywhere in the program - the most cohesive place for a rule is next to the data it protects.
Example 2 - reducing coupling
The next example calculates employee bonuses. The first version has three kinds of coupling: it reads apublic static rate that any class can change (common coupling), it takes a flag from the caller that decides the calculation (control coupling), and it takes a whole Employee although it needs only the salary (stamp coupling). The second version removes all three.
Java code showing tight coupling and loose coupling in a bonus calculator
//CouplingExample.java
public class CouplingExample {
record Employee(String name, double salary, boolean manager) {}
// ---------- Tightly coupled version ----------
static class Settings {
static double bonusRate = 0.10; // global and mutable: common coupling
}
static class TightBonusCalculator {
// the flag from the caller decides the calculation: control coupling
double bonus(Employee e, boolean doubleIt) {
double rate = Settings.bonusRate;
if (doubleIt) rate = rate * 2;
return e.salary() * rate;
}
}
// Unrelated code elsewhere that "tweaks" the shared setting
static class YearEndCampaign {
void start() { Settings.bonusRate = 0.25; }
}
// ---------- Loosely coupled version ----------
interface BonusPolicy { double bonusFor(double salary); }
record FlatRate(double rate) implements BonusPolicy {
public double bonusFor(double salary) { return salary * rate; }
}
static class BonusCalculator {
private final BonusPolicy staffPolicy;
private final BonusPolicy managerPolicy;
BonusCalculator(BonusPolicy staffPolicy, BonusPolicy managerPolicy) {
this.staffPolicy = staffPolicy;
this.managerPolicy = managerPolicy;
}
double bonus(Employee e) {
BonusPolicy policy = e.manager() ? managerPolicy : staffPolicy;
// passes only what the policy needs: data coupling
return policy.bonusFor(e.salary());
}
}
public static void main(String[] args) {
Employee tom = new Employee("Tom Jones", 50_000, false);
Employee nancy = new Employee("Nancy Smith", 80_000, true);
TightBonusCalculator tight = new TightBonusCalculator();
BonusCalculator loose = new BonusCalculator(new FlatRate(0.10), new FlatRate(0.20));
System.out.printf("Before campaign - tight: %.2f, %.2f | loose: %.2f, %.2f%n",
tight.bonus(tom, false), tight.bonus(nancy, true),
loose.bonus(tom), loose.bonus(nancy));
new YearEndCampaign().start();
System.out.printf("After campaign - tight: %.2f, %.2f | loose: %.2f, %.2f%n",
tight.bonus(tom, false), tight.bonus(nancy, true),
loose.bonus(tom), loose.bonus(nancy));
}
}
Before campaign - tight: 5000.00, 16000.00 | loose: 5000.00, 16000.00 After campaign - tight: 12500.00, 40000.00 | loose: 5000.00, 16000.00
Explanation of the code
TightBonusCalculatorreadsSettings.bonusRate, a global thatYearEndCampaignchanges. The calculator's results changed even though no one touched the calculator - the two classes are coupled through shared data that neither of them owns.- The caller of
tight.bonus()must know that managers get double, and passtruefor them. That rule is now spread across every caller, which is what control coupling does. BonusCalculatorreceives its policies through the constructor. Its behaviour is fixed when it is created, and nothing outside can change it later, so the campaign has no effect on it.BonusPolicy.bonusFor()takes adouble, not anEmployee. Policies can be reused for anything with a salary, and tested with plain numbers.- The manager rule now lives in one place, inside
BonusCalculator, instead of in every caller. Code that changes together is kept together - higher cohesion as a side effect of lower coupling.
How to spot coupling and cohesion problems
- The "and" test: if a class needs "and" to describe what it does, its cohesion is probably low.
- Fields used by few methods: if each group of methods uses its own group of fields, the class is really two or more classes. Metrics such as LCOM (lack of cohesion of methods) measure this.
- Shotgun surgery: one small change needs edits in many classes - coupling is high, or cohesion is low.
- Long import lists and constructors: a class with many dependencies is coupled to all of them.
- Hard to test: if a unit test needs a database, a clock and three other services, the class is tightly coupled to them.
- Package dependencies: the JDK's
jdepstool lists the packages each class depends on, and cycles between packages are a strong sign of tight coupling.
Techniques that lower coupling
- Depend on interfaces, and receive implementations through constructors - see the Dependency Inversion PrincipleClick to read: Dependency Inversion Principle.
- Pass data, not objects, when a method needs only part of an object.
- Replace flags with separate methods or with polymorphism.
- Avoid global mutable state: make shared values constants, or pass them in.
- Hide internals: return copies or read-only views, and do not chain through other objects' getters - see the Law of DemeterClick to read tutorial: Law of Demeter in Java Explained with Examples.
- Use Java modules to make the allowed dependencies between packages explicit - see the Java 9 Modules tutorialClick to read: Java 9 Modules tutorial.
Summary
In this tutorial we first defined cohesion, how well the parts inside a module belong together, and coupling, how much a module depends on others, and saw why the goal is high cohesion with low coupling. Next we looked at the kinds of each, from functional cohesion down to coincidental, and from data coupling down to content coupling. We then split aUtils class into cohesive types, and refactored a bonus calculator to remove common, control and stamp coupling - and saw the tightly coupled version change its results because of unrelated code. Lastly, we listed the signs of coupling and cohesion problems and the techniques that fix them.Programming Principles Tutorials
Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC Law of Demeter in Java Explained with ExamplesClick to read tutorial: Law of Demeter in Java Explained with Examples Tell, Don't Ask Principle Explained with Java ExamplesClick to read tutorial: Tell, Don't Ask Principle Explained with Java Examples Separation of Concerns Explained with Java ExamplesClick to read tutorial: Separation of Concerns Explained with Java Examples
Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC Law of Demeter in Java Explained with ExamplesClick to read tutorial: Law of Demeter in Java Explained with Examples Tell, Don't Ask Principle Explained with Java ExamplesClick to read tutorial: Tell, Don't Ask Principle Explained with Java Examples Separation of Concerns Explained with Java ExamplesClick to read tutorial: Separation of Concerns Explained with Java Examples