Skip to main content
A pixel art illustration showing the contrast between high coupling (left side with tangled connections between buildings) and low coupling with high cohesion (right side with clean, organized buildings each with a single clear purpose). The image illustrates how good software design minimizes dependencies between modules while maximizing the focus of each module.

CS 3100: Program Design and Implementation II

Lecture 7: Changeability II — Coupling and Cohesion

©2025 Jonathan Bell & Ellen Spertus, CC-BY-SA

Learning Objectives

After this lecture, you will be able to:

  1. Analyze the changeability of a software module for some hypothetical change using the language of coupling and cohesion
  2. Define and recognize cases of data coupling, stamp coupling, control coupling, common coupling, and content coupling
  3. Define and recognize cases of coincidental, logical, temporal, procedural, communicational, sequential, and functional cohesion
  4. Use the vocabulary of coupling and cohesion to review the Strategy pattern

Poll: What are the pillars of OOP?

A. Abstraction

B. Encapsulation

C. Inheritance

D. Litigation

E. Marble

F. Polymorphism

G. Polyphemus

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

The Four Pillars of OOP

The four pillars of OOP: Abstraction, Encapsulation, Inheritance, and Polymorphism

Coupling and Cohesion Measure How Easy Code is to Change

You should recall these terms from CS 2100:

  • Coupling: How much a module is affected by changes in other modules
  • Cohesion: How closely related the elements internal to a module are

These terms apply at any scale: methods, classes, or entire systems.

High Coupling Creates a Ripple Effect of Changes

Illustration showing the ripple effect of high coupling - one module being changed causes red shockwaves that break multiple surrounding modules, like dominoes falling in a chain reaction.
  • Changes to B may require changes to A
  • You must understand more code to make any change
  • Higher risk of introducing bugs
  • More likely to conflict with other developers

Goal: Minimize coupling to isolate changes.

Low Cohesion Creates a "Junk Drawer" of Unrelated Code

Illustration of a junk drawer labeled 'UtilityClass.java' containing random unrelated items (hammer, banana, keys, book, sock, pizza) with a confused developer asking 'Where does the new spatula go?!' - illustrating low cohesion where unrelated things are grouped together.
  • The code appears unorganized
  • Methods seem unrelated to each other
  • The module's "responsibility" is unclear

Goal: Each module should have a single, clear purpose.

Good Design: Clear Responsibilities with Minimal Dependencies

Requirement: Allow graders to annotate student submissions with feedback.

Bad Design: Circular Dependencies and Mixed Responsibilities

What's wrong with this?

Coupling Exists on a Spectrum from Safe to Dangerous

A spectrum showing five types of coupling from safe (green) to dangerous (red): Data Coupling (passing simple boxes), Stamp Coupling (passing complex packages), Control Coupling (switches controlling flow), Common Coupling (modules sharing a central database), and Content Coupling (crowbar breaking into another module's internals).

Let's look more closely at each of these...

Data Coupling: Pass Only What You Need

Modules share only primitive types or standard library types.

class EmailService {
void sendNotification(String toEmail, String studentName, String assignmentName) {
// Only receives exactly what it needs - no unused data
String body = "Hi " + studentName + ", you received feedback on "
+ assignmentName;
send(toEmail, "New Feedback", body);
}
}

✓ Types are unlikely to change (String, int, ArrayList, etc.)

Stamp Coupling: Passing Entire Objects Creates Hidden Dependencies

Illustration showing stamp coupling: a sender module stamps a complete complex document (Submission object with many fields), but the receiver module only needs 2-3 fields, thinking 'I only needed the email...' - illustrating how stamp coupling passes more data than necessary.
class EmailService {
void sendNotification(Submission submission) {
// Only uses 3 fields but receives entire complex structure
String toEmail = submission.student.email; // ✓ Used
String name = submission.student.name; // ✓ Used
String assignmentName = submission.assignment.name; // ✓ Used
String body = "Hi " + name + ", you received feedback on " + assignmentName;
send(toEmail, "New Feedback", body);
}
}

⚠ EmailService depends on the Submission structure.

Why "stamp"? Like being coupled to the exact stamp on an envelope with a letter inside—you're coupled to its specific format, which might change.

Control Coupling: Parameters That Control Flow Violate Single Responsibility

A parameter controls the flow of execution inside the method.

public void sendNotification(User destination, String message, DeliveryType type) {
if (type == DeliveryType.EMAIL) {
// Send email
} else if (type == DeliveryType.SMS) {
// Send SMS
} else if (type == DeliveryType.PUSH) {
// Send push notification
} else if (type == DeliveryType.IN_APP) {
// Send in-app notification
} else if (type == DeliveryType.CANVAS) {
// Send Canvas notification
}
}

The caller already knows which branch to take—it might as well have the logic directly. And you can't safely change this method without knowing how every caller uses it.

Separate Methods Let Callers Call What They Mean

Instead of one method with a control argument, use separate methods:

public void sendPushNotification(User destination, String message);
public void sendInAppNotification(User destination, String message);
public void sendCanvasNotification(User destination, String message);
public void sendEmailNotification(User destination, String message);
public void sendSmsNotification(User destination, String message);

This works if we want the caller to determine how to send the notification. But there's an even better approach—the Strategy pattern—which we'll see after discussing cohesion.

Common Coupling: Global State Creates Invisible Dependencies

⚠ We're now entering "danger zone" coupling—much harder to manage than the previous types.

Multiple modules share a common data structure (often global).

class Assignment {
// A global map that stores all assignments and their submissions.
public static Map<Assignment, List<Submission>> submissions = new HashMap<>();
// ...
}

class NotificationService {
public static void sendGradeReleaseNotifications(Assignment assignment) {
List<Submission> submissions = Assignment.submissions.get(assignment);
...
}
}

Content Coupling Breaks Encapsulation

The use of the private keyword and defensive copying should protect our data.

class Assignment {
private List<Submission> submissions = new ArrayList<>();

public void addSubmission(Submission submission) {
submissions.add(submission);
}

public List<Submission> getSubmissions() {
return new ArrayList<>(submissions); // Returns a copy
}
}

Information hiding helps limit content coupling

The Secret Truth: Reflection Bypasses Enforcement

Two-panel comic. Left panel titled 'What we told you last lecture' shows Python as an open door with a friendly 'Keep Out (Or Enter. I'm a sign, not a cop)' sign, while Java has an armored guard at a locked gate marked 'Private'. Right panel titled 'The secret truth about Java' shows the same Java gate, but now someone bribes the guard with a bag labeled 'setAccessible(true)', another person flashes a 'VIP Reflection API' badge, and a third sneaks through a hole in the wall.

Example of Content Coupling Using Reflection

class SubmissionReplacementService {
public static void replaceSubmission(Assignment assignment,
Submission oldSub,
Submission newSub) {
// Use reflection to access the private submissions field
Field submissionsField = Assignment.class.getDeclaredField("submissions");
submissionsField.setAccessible(true);
List<Submission> submissions = (List<Submission>) submissionsField.get(assignment);
submissions.remove(oldSub);
submissions.add(newSub);
}
}

✗ Ignores the interface
✗ Changes to Assignment will silently break this
✗ "Find usages" won't find this dependency!

Why Does Java Provide Reflection?

Definition: The ability to inspect code/objects running in the same JVM

Legitimate uses:

  • Serialization (writing out objects for storage/transmission)
  • Deserialization (converting back to objects)
  • Test frameworks
  • Debuggers

Java Reflection (meme)

Meme with heading 'Me after learning reflection in Java'.
Astronaut 1 (viewing Earth): 'Wait, even the classes are objects?'
Astronaut 2 (pointing gun): 'Always has been'

Coupling Summary

  • Data coupling shares common types, such as String
  • Stamp coupling shares user-defined types that might change
  • Control Coupling happens when a parameter to a method controls the flow of execution
  • Common Coupling involves multiple modules sharing a common data structure without proper encapsulation
  • Content Coupling violates encapsulation

Poll: What type of coupling is this? 1

class PermissionChecker {
boolean canAccess(User u, Resource r,
String type) {
if (type.equals("READ")) {
return u.hasReadPermission(r);
} else if (type.equals("WRITE")) {
return u.hasWritePermission(r);
} else if (type.equals("DELETE")) {
return u.hasDeletePermission(r);
} else if (type.equals("ADMIN")) {
return u.isAdmin();
}
return false;
}
}

A. data coupling (standard data type)

B. stamp coupling (user-defined data type)

C. control coupling (parameter controls execution)

D. common coupling (access to shared class)

E. content coupling (reflection)

Poll: What type of coupling is this? 2

void processOrder(Order order) {
String customerName = order.customer.name;
String customerEmail = order.customer.email;
String productName = order.items.get(0).product.name;
// process using only these three values
}

A. data coupling (standard data type)

B. stamp coupling (user-defined data type)

C. control coupling (parameter controls execution)

D. common coupling (access to shared class)

E. content coupling (reflection)

Discussion: How can coupling be reduced?

class OrderProcessor {
void processOrder(Order order) {
String customerName = order.customer.name;
String customerEmail = order.customer.email;
String productName = order.items.get(0).product.name;
// process using only these three values
}
}

Cohesion is a Ladder: Aim for the Top

Illustration showing cohesion as a ladder to climb: from bottom (red) Coincidental (junk drawer) through Logical (toolbox with electrical, plumbing, carpentry tools - same category but NOT interchangeable, marked with 'Can't swap!' between sections), Temporal, Procedural, Communicational, Sequential, up to the top (green) Functional (one focused worker with 'One Job, Done Well!' banner). An arrow says 'AIM FOR THE TOP' as a character climbs upward.

Coincidental Cohesion: "Utility" Classes are Code Smell

Parts are grouped together by accident—no real relationship.

class Utility {
public static String formatNameForSorting(String firstName, String lastName) {
return String.format("%s, %s", lastName, firstName);
}
public static String formatDueDate(Date dueDate) {
return String.format(Locale.getDefault(), "Due on %tB %<te, %<tY", dueDate);
}
public static String celsiusToFahrenheit(double celsius) {
return String.format("%.2f°F", celsius * 9 / 5 + 32);
}
public static boolean isInstructorForStudent(Instructor instructor, Student student) {
return instructor.getStudents().contains(student);
}
}

Logical Cohesion: Same Category ≠ Same Responsibility

Parts perform logically similar tasks.

class Formatter {
public static String formatNameForSorting(String firstName, String lastName) {
return String.format("%s, %s", lastName, firstName);
}
public static String formatNameForDisplay(String firstName, String lastName) {
return String.format("%s %s", firstName, lastName);
}
public static String formatDueDate(Date dueDate) {
return String.format(Locale.getDefault(), "Due on %tB %<te, %<tY", dueDate);
}
}

Better: All methods format something. But name formatting and date formatting are still unrelated.

Temporal Cohesion: "Runs Together" is Weak Justification

Parts are grouped because they are executed at the same time.

class SystemLifecycle {
public static void initialize() {
createSubmissionService();
createRegradeService();
setupDatabase();
setupWebServer();
setupEmailService();
setupCanvasService();
}

private static void createSubmissionService() { /* ... */ }
private static void createRegradeService() { /* ... */ }
private static void setupDatabase() { /* ... */ }
private static void setupWebServer() { /* ... */ }
private static void setupEmailService() { /* ... */ }
private static void setupCanvasService() { /* ... */ }
}

Procedural Cohesion: Steps in a Workflow Belong Together

Parts follow a procedure together (they're related steps).

class SubmissionService {
public void processSubmission(Submission submission) {
TestResult testResult = runTests(submission);
LintResult lintResult = lintSubmission(submission);
GradingResult gradeResult = gradeSubmission(submission, testResult, lintResult);
saveSubmission(submission, gradeResult);
}

private TestResult runTests(Submission submission) { /* ... */ }
private LintResult lintSubmission(Submission submission) { /* ... */ }
private GradingResult gradeSubmission(Submission sub, TestResult t, LintResult l) { /* ... */ }
private void saveSubmission(Submission submission, GradingResult gradeResult) { /* ... */ }
}

Communicational Cohesion: Operating on Shared Data Creates Relatedness

Parts operate on the same data. (Same example as procedural cohesion!)

class SubmissionService {
public void processSubmission(Submission submission) {
TestResult testResult = runTests(submission);
LintResult lintResult = lintSubmission(submission);
GradingResult gradeResult = gradeSubmission(submission, testResult, lintResult);
saveSubmission(submission, gradeResult);
}
// All methods operate on Submission objects
}

This is getting stronger—the methods are clearly related by their shared data.

Sequential Cohesion: Pipelines Have Clear Data Flow

The output of one part is the input to another.

public void processSubmission(Submission submission) {
TestResult testResult = runTests(submission); // Output: testResult
LintResult lintResult = lintSubmission(submission); // Output: lintResult
GradingResult gradeResult = gradeSubmission( // Input: testResult, lintResult
submission, testResult, lintResult); // Output: gradeResult
saveSubmission(submission, gradeResult); // Input: gradeResult
}

Key: gradeSubmission must be called after runTests and lintSubmission.

This is strong cohesion: the data dependencies make the ordering explicit, and you can trace exactly how information flows through the module.

Functional Cohesion: One Module, One Job, Done Well

All parts contribute to a single, well-defined task.

This is the goal: each module has a clear, single purpose you can describe in one sentence. Easy to understand, test, and modify independently.

Strategy Pattern Reduces Coupling and Increases Cohesion

Recall the Strategy pattern: encapsulate interchangeable algorithms behind a common interface. Each variant gets its own class.

Split-screen comparison: Left shows 'Without Strategy' - one overloaded robot trying to handle Java, Python, TypeScript all at once, overheating and stressed. Right shows 'With Strategy' - a clean coordinator robot delegating to separate specialist robots (JavaStrategy, PythonStrategy, TypeScriptStrategy) each doing one job well, easily adding new RustStrategy.

Strategy Fixes Control Coupling by Hoisting the Decision

Remember the control coupling problem? Strategy pattern fixes it:

void notifyUser(User user, String message,
NotificationSender sender) {
sender.send(user, message);
}

The choice of which notification to send is now made when the NotificationSender is created—not buried inside the method.

Strategy Increases Cohesion by Isolating Each Variant

Each strategy class has functional cohesion—one language, one job. Adding TypeScript? Just add a new class.

Benefits of Strategy Pattern

AspectWithout StrategyWith Strategy
CouplingJava/Python code coupled in same classEach strategy isolated; only interface shared
CohesionLow—class does many unrelated thingsHigh—each class has one job
Adding languagesModify SubmissionServiceAdd new strategy class
TestingMust test all paths togetherTest each strategy in isolation

⚠ But beware: Strategy adds indirection and more classes. Use it when you actually have multiple variants—not for hypothetical future flexibility.

What's the difference between polymorphism and Strategy?

Polymorphism is the mechanism—calling a method on an abstract type, where the concrete runtime type determines which implementation runs.

Strategy is a design pattern that uses polymorphism to make algorithms interchangeable by holding a reference to the concrete subtype.

Key Takeaways (image)

Summary showing two key goals: Left panel 'LOW COUPLING' shows two modules with minimal connection where changes to one don't affect the other. Right panel 'HIGH COHESION' shows a focused 'EmailService' module with all related components neatly organized inside. Bottom banner: 'Minimize Dependencies. Maximize Focus.'

Key Takeaways

  • Low coupling = changes stay localized
  • High cohesion = modules have clear purposes
  • Prefer data coupling over stamp, control, common, or content
  • Aim for functional cohesion where each module does one thing