Loading learning content...
You've successfully extracted classes and split responsibilities. The original bloated class is now three focused classes. Victory, right?
Not so fast. Extraction introduces a subtle risk: cohesion degradation. Each new class must be cohesive in its own right, and the relationships between classes must preserve the overall system's coherence. A poorly executed split can create classes that are internally fragmented—nominally focused on one responsibility but actually containing unrelated elements that ended up together by accident.
Maintaining cohesion after a split is the difference between a successful refactoring and a reorganization of chaos into differently located chaos.
By the end of this page, you will understand how to measure and verify cohesion in newly created classes, recognize signs that a split has damaged cohesion, apply techniques to restore cohesion when degradation is detected, and design splits that naturally produce cohesive results from the start.
Cohesion measures how closely the elements of a module (in our case, a class) belong together. High cohesion means every field and method contributes to a single, unified purpose. Low cohesion means the class is a grab-bag of loosely related or unrelated elements.
Types of Cohesion (from worst to best):
| Type | Description | Example | Quality |
|---|---|---|---|
| Coincidental | Elements grouped arbitrarily | Utilities class with random static methods | Worst |
| Logical | Elements grouped by category (all validators) | ValidationUtils with unrelated validators | Poor |
| Temporal | Elements grouped by when they run | InitializationManager with startup tasks | Poor |
| Procedural | Elements grouped by execution order | OrderWorkflow with sequential steps | Moderate |
| Communicational | Elements operate on same data | Methods all using User object | Good |
| Sequential | Output of one is input to next | Data transformation pipeline | Good |
| Functional | All elements contribute to single task | PasswordHasher with hash and verify | Best |
The SRP-Cohesion Connection:
SRP and cohesion are two sides of the same coin:
A class that follows SRP will naturally have high cohesion. If a class has one reason to change, all its elements must contribute to that single responsibility—that's functional or near-functional cohesion.
Conversely, high cohesion helps ensure SRP. If all elements work together toward one goal, they'll be affected by the same types of changes.
You should be able to describe what a class does in a single sentence without using conjunctions like 'and' or 'or'. 'The PasswordHasher hashes passwords and verifies password matches' uses 'and' but describes a single coherent purpose. 'The UserService manages authentication and sends emails and generates reports' reveals three distinct responsibilities.
While cohesion ultimately requires human judgment, several quantitative metrics help identify cohesion problems. Understanding these metrics enables data-driven refactoring decisions.
LCOM (Lack of Cohesion in Methods)
LCOM measures how many method pairs don't share any instance fields. High LCOM indicates low cohesion—methods that don't work with the same data probably handle different responsibilities.
123456789101112131415161718192021222324252627
// LCOM Calculation Examplepublic class OrderProcessor { private ValidationRules rules; // Field A private List<String> errors; // Field B private ShippingZone zone; // Field C private Map<String, Double> rates; // Field D // Methods and their field usage: // validate() uses A, B → Cluster 1 // getErrors() uses B → Cluster 1 // calcShipping() uses C, D → Cluster 2 // getZone() uses C → Cluster 2 // Method pairs share NO fields: // validate() & calcShipping() → different clusters // validate() & getZone() → different clusters // getErrors() & calcShipping() → different clusters // getErrors() & getZone() → different clusters // Method pairs share fields: // validate() & getErrors() → share B // calcShipping() & getZone() → share C // LCOM = pairs with no shared fields - pairs with shared fields // LCOM = 4 - 2 = 2 (positive = low cohesion!) // This class should be split into two classes}TCC (Tight Class Cohesion) and LCC (Loose Class Cohesion)
These metrics examine method connectivity:
A TCC above 0.5 suggests good cohesion. Below 0.3 indicates problems.
NCSI (Number of Classes Sharing Fields)
After a split, check: how many of your new classes share state or have bidirectional dependencies? Ideally, extracted classes should be independent or have clean, unidirectional dependencies.
| Metric | Healthy Range | Warning Zone | Problem Zone |
|---|---|---|---|
| LCOM | 0 or negative | 1-3 | 3 |
| TCC | 0.5 | 0.3-0.5 | <0.3 |
| LCC | 0.7 | 0.5-0.7 | <0.5 |
| Methods per Class | 5-15 | 15-25 | 25 or <3 |
| Fields per Class | 3-8 | 8-12 | 12 or 0 |
No metric perfectly captures cohesion. A class with high LCOM might be well-designed (e.g., a Facade that delegates to multiple subsystems). Use metrics as starting points for investigation, not as absolute judgments.
After extracting classes or splitting responsibilities, systematically verify that each resulting class maintains high cohesion. This verification should happen immediately after the split, not weeks later.
Verification Checklist for Each New Class:
PaymentValidator shouldn't have formatReceipt().12345678910111213141516171819202122232425262728293031323334353637383940414243
// Post-split cohesion analysis examplepublic class OrderValidator { private final ValidationRules rules; private final List<String> errors = new ArrayList<>(); private boolean validated = false; // ✓ Uses 'rules' and 'errors' - connected to class purpose public boolean validate(Order order) { errors.clear(); for (Rule rule : rules.getRules()) { if (!rule.check(order)) { errors.add(rule.getMessage()); } } validated = errors.isEmpty(); return validated; } // ✓ Uses 'errors' - directly related to validation state public List<String> getErrors() { return new ArrayList<>(errors); } // ✓ Uses 'validated' - validation state accessor public boolean isValidated() { return validated; } // ✗ PROBLEM: Uses no instance fields! public String formatErrorReport(List<String> errors) { return String.join("\n", errors); } // This method doesn't belong here - it's a formatting concern // Should move to an ErrorFormatter class or be a static utility} // Corrected version after removing non-cohesive methodpublic class OrderValidator { private final ValidationRules rules; private final List<String> errors = new ArrayList<>(); private boolean validated = false; public boolean validate(Order order) { /* ... */ } public List<String> getErrors() { /* ... */ } public boolean isValidated() { /* ... */ } // All methods now use instance state - high cohesion ✓}Look at your class's constructor. If it initializes fields that some methods never use, those fields and their methods might belong in a different class. A cohesive class's constructor sets up state that the entire class uses together.
Cohesion often degrades gradually. A newly extracted class starts focused, but over time, developers add "just one more method" until it's bloated again. Recognizing degradation early prevents repeating the extraction cycle.
Warning Signs of Post-Split Cohesion Decay:
Case Study: Cohesion Decay in Action
Let's trace how a focused class degrades over six months of feature development:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// Month 1: Clean, focused class after extractionpublic class EmailSender { private final SmtpClient smtp; private final EmailTemplates templates; public void sendWelcome(User user) { /* ... */ } public void sendPasswordReset(User user, Token token) { /* ... */ } public void sendOrderConfirmation(Order order) { /* ... */ }} // Month 2: Added logging "just for debugging"public class EmailSender { private final SmtpClient smtp; private final EmailTemplates templates; private final Logger logger; // ← New dependency public void sendWelcome(User user) { logger.info("Sending welcome email to " + user.getId()); /* ... */ } // Logging is cross-cutting, but still manageable} // Month 3: Added retry logic for reliabilitypublic class EmailSender { private final SmtpClient smtp; private final EmailTemplates templates; private final Logger logger; private final RetryPolicy retryPolicy; // ← Reliability concern private final int maxRetries; // ← Configuration concern // Methods now include retry loops...} // Month 5: Added analytics trackingpublic class EmailSender { private final SmtpClient smtp; private final EmailTemplates templates; private final Logger logger; private final RetryPolicy retryPolicy; private final int maxRetries; private final AnalyticsClient analytics; // ← Analytics concern private final boolean trackOpens; // ← Feature flag public void sendWelcome(User user) { // Now includes: sending, logging, retrying, tracking // Four responsibilities in one class! }} // Month 6: The class is now a mini-monolith again// Time to re-extract: ReliableEmailSender, EmailAnalytics, etc.Each individual addition seems harmless. 'It's just logging.' 'It's just one retry loop.' But cumulative small additions erode cohesion. Establish team norms: any new field or method must justify its relationship to the class's core purpose. If it can't, it goes elsewhere.
When cohesion has degraded, it's time to refactor again. The goal is to return to a state where each class has high internal cohesion. This isn't failure—it's normal maintenance.
Cohesion Restoration Techniques:
Technique: Secondary Extraction
Apply the same extraction process you learned earlier. Identify the added responsibilities and extract them into new classes.
From the EmailSender example:
RetryPolicy handling into a ReliableMessageSender wrapperEmailAnalytics serviceEmailSender focused only on email composition and sending123456789101112131415161718192021222324252627282930313233
// After cohesion restoration via secondary extraction // Core responsibility only - high cohesion restoredpublic class EmailSender { private final SmtpClient smtp; private final EmailTemplates templates; public void send(Email email) { String body = templates.render(email.template(), email.data()); smtp.send(email.to(), email.subject(), body); }} // Retry logic extracted - single responsibilitypublic class ReliableEmailSender { private final EmailSender sender; private final RetryPolicy policy; public void send(Email email) { policy.execute(() -> sender.send(email)); }} // Analytics extracted - single responsibilitypublic class AnalyticsDecoratedSender { private final EmailSender sender; private final EmailAnalytics analytics; public void send(Email email) { sender.send(email); analytics.trackSent(email); }}Restoring cohesion is more expensive than maintaining it. Establish practices that prevent degradation in the first place.
Preventive Practices:
1234567891011121314151617181920
/** * PURPOSE: Handles password security operations. * * RESPONSIBILITY: Hashing passwords, verifying password matches, * checking password strength. * * NOT RESPONSIBLE FOR: * - Storing passwords (see UserRepository) * - Password reset flow (see PasswordResetService) * - Authentication decisions (see AuthenticationService) * * CHANGE REASONS: Password hashing algorithm updates, * strength requirement changes. * * @author Security Team */public class PasswordService { // Methods must fit the stated responsibility // Anything else needs discussion before adding}What gets measured gets managed. Display cohesion metrics on dashboards. Make it easy for anyone to see which classes might need attention. Visibility creates accountability without requiring heavy process.
Cohesion isn't just about individual classes—it's about how related classes work together. After splitting a God Class into multiple focused classes, ensure the resulting "family" of classes maintains coherent relationships.
Package-Level Cohesion:
Classes extracted from the same original should typically live in the same package. They form a cohesive module with internal collaborations and external boundaries.
123456789101112131415161718192021222324252627
// Package structure after splitting OrderProcessor package com.example.orders.processing; // Public: Package's external APIpublic class OrderProcessor { private final OrderValidator validator; // Package-private private final ShippingCalculator shipping; // Package-private public ProcessedOrder process(Order order) { /* orchestrates */ }} // Package-private: Implementation detailsclass OrderValidator { ValidationResult validate(Order order) { /* ... */ }} class ShippingCalculator { ShippingCost calculate(Order order) { /* ... */ }} // Benefits:// 1. Package forms a cohesive module// 2. Internal classes can collaborate freely// 3. External API is minimal (just OrderProcessor)// 4. Encapsulation at package level prevents misuse// 5. Related classes change together, versioned togetherRelationship Cohesion Checks:
Dependency Direction — Do all dependencies point in a consistent direction? Circular dependencies indicate poor separation.
Interface Stability — Are the interfaces between extracted classes stable? Constantly changing interfaces suggest the split boundaries are wrong.
Joint Reuse — Would you ever want to use one class without its siblings? If not, maybe they should be merged or clearly packaged together.
Independent Testability — Can each class be unit tested in isolation? If testing one requires setting up three others, coupling is too high.
Classes that change together should be packaged together. If extracting a class means it always changes when its original changes, they should stay in the same package or possibly remain one class. Package boundaries should align with change boundaries.
Extracting classes and splitting responsibilities is only half the battle. Maintaining cohesion in the resulting classes—and preventing degradation over time—ensures the refactoring provides lasting value.
What's next:
With extraction, splitting, and cohesion maintenance mastered, the final page brings it all together: a step-by-step SRP refactoring guide that walks through a complete, real-world refactoring from initial analysis through final verification.
You now understand how to measure, verify, and maintain cohesion in extracted classes. Cohesion is the internal quality that makes SRP work—without it, even well-intentioned splits create fragmented, hard-to-maintain code.