
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:
- Analyze the changeability of a software module for some hypothetical change using the language of coupling and cohesion
- Define and recognize cases of data coupling, stamp coupling, control coupling, common coupling, and content coupling
- Define and recognize cases of coincidental, logical, temporal, procedural, communicational, sequential, and functional cohesion
- 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

Text espertus to 22333 if the
URL isn't working for you.
The Four Pillars of OOP

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

- 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

- 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

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

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

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)

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

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.

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
| Aspect | Without Strategy | With Strategy |
|---|---|---|
| Coupling | Java/Python code coupled in same class | Each strategy isolated; only interface shared |
| Cohesion | Low—class does many unrelated things | High—each class has one job |
| Adding languages | Modify SubmissionService | Add new strategy class |
| Testing | Must test all paths together | Test 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)

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