Loading learning content...
So far, we've discussed responsibility from a negative perspective: avoiding multiple actors, preventing unrelated changes from coupling together. But how do we know when a class is designed well, not just not badly?
The answer is cohesion—one of the oldest and most fundamental concepts in software engineering. Cohesion measures how strongly the elements within a module belong together. High cohesion indicates a well-designed responsibility; low cohesion suggests a class doing too many unrelated things.
Cohesion is the positive test for SRP. Instead of asking "does this class have multiple reasons to change?" we ask "do all elements of this class work together toward a single purpose?"
By the end of this page, you will understand the different types of cohesion, how to measure cohesion in your classes, and how to use cohesion analysis to guide refactoring decisions. You'll gain practical techniques for improving class design through cohesion assessment.
Cohesion describes the degree to which elements of a module (methods and fields in a class) belong together. The term originates from Larry Constantine's structured design methodology in the 1970s, but remains central to object-oriented design today.
The intuition:
Think of a class as a team. A highly cohesive team has members who all work toward the same goal, using shared resources, communicating constantly. A low-cohesion team has members working on unrelated projects who barely interact—they're co-located but not truly collaborating.
In code terms:
Cohesion and SRP are two sides of the same coin:
A class following SRP will naturally have high cohesion. A class with high cohesion will naturally follow SRP. Violating one implies violating the other.
Constantine and Yourdon identified seven levels of cohesion, ranked from worst to best. Understanding this spectrum helps you evaluate and improve your designs:
| Level | Type | Description | Example |
|---|---|---|---|
| 1 (Worst) | Coincidental | Elements are grouped arbitrarily with no meaningful relationship | Utilities class with unrelated static methods |
| 2 | Logical | Elements grouped by category but perform different operations | InputHandler that handles keyboard, mouse, and file input |
| 3 | Temporal | Elements grouped because they execute at the same time | StartupInitializer that configures logging, database, and cache |
| 4 | Procedural | Elements grouped because they follow a sequence | OrderProcessor with validate → save → email sequence |
| 5 | Communicational | Elements grouped because they operate on the same data | CustomerRecord with load/save/format for customer data |
| 6 | Sequential | Output of one element feeds directly into another | DataPipeline where parse → transform → serialize chain |
| 7 (Best) | Functional | All elements contribute to a single, well-defined task | PasswordValidator focused solely on password validation |
Analyzing the spectrum:
Coincidental Cohesion (Level 1) is the worst because elements are together purely by accident. The classic Utilities or Helpers class falls here—a dumping ground for methods that don't fit elsewhere. These classes violate SRP by definition: they have as many reasons to change as they have unrelated methods.
Logical Cohesion (Level 2) groups things that seem similar but aren't functionally related. An InputHandler that handles keyboard, mouse, and file input might seem logical, but keyboard input changes for different reasons than file input.
Temporal and Procedural Cohesion (Levels 3-4) group by when things happen rather than what they do. These are common in older codebases and often need refactoring.
Communicational and Sequential Cohesion (Levels 5-6) are stronger because elements share data and workflows. These often represent genuine business processes.
Functional Cohesion (Level 7) is the goal: every element contributes to exactly one well-defined function. This aligns perfectly with SRP—one purpose, one reason to change.
In object-oriented design, strive for functional or communicational cohesion. If you can describe what your class does in a single verb phrase without conjunctions ("validates passwords", "calculates shipping costs", "manages user sessions"), you likely have functional cohesion.
While the cohesion spectrum is conceptual, we can measure cohesion more precisely using structural analysis. One of the most useful metrics is LCOM (Lack of Cohesion of Methods).
LCOM Intuition:
LCOM counts how many pairs of methods in a class don't share instance variables. If methods A and B use completely different fields, they're probably doing unrelated things.
LCOM Calculation (Simplified):
S)P)An LCOM of 0 indicates high cohesion (all methods share data). A high LCOM indicates low cohesion (methods operate on disjoint data).
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// HIGH COHESION (LCOM = 0)// All methods use the same fieldsclass Rectangle { private width: number; private height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } // Uses: width, height getArea(): number { return this.width * this.height; } // Uses: width, height getPerimeter(): number { return 2 * (this.width + this.height); } // Uses: width, height getDiagonal(): number { return Math.sqrt(this.width ** 2 + this.height ** 2); } // Uses: width, height isSquare(): boolean { return this.width === this.height; }}// Method pairs that share variables: 6 (all pairs share width AND height)// Method pairs that share nothing: 0// LCOM = max(0, 0 - 6) = 0 ✓ // LOW COHESION (LCOM = 3)// Methods use disjoint sets of fieldsclass UserManager { // Group A fields private emailService: EmailService; private emailTemplates: Map<string, string>; // Group B fields private database: Database; private connectionPool: ConnectionPool; // Group A methods - use emailService, emailTemplates sendWelcomeEmail(user: User): void { const template = this.emailTemplates.get('welcome'); this.emailService.send(user.email, template); } sendPasswordReset(user: User): void { const template = this.emailTemplates.get('reset'); this.emailService.send(user.email, template); } // Group B methods - use database, connectionPool saveUser(user: User): void { const conn = this.connectionPool.acquire(); this.database.insert('users', user, conn); } findUserById(id: string): User | null { const conn = this.connectionPool.acquire(); return this.database.find('users', id, conn); }}// Method pairs that share variables: 2 (within groups)// Method pairs that share nothing: 4 (between groups)// LCOM = max(0, 4 - 2) = 2 ✗ (suggests splitting)The Field-Method Matrix:
A practical technique for visualizing cohesion is the field-method matrix. Create a table with methods as rows and fields as columns. Mark each cell where a method uses a field.
| width | height | emailService | database | |
|---|---|---|---|---|
| getArea() | ✓ | ✓ | ||
| sendEmail() | ✓ | |||
| saveUser() | ✓ |
In a highly cohesive class, marks form a dense block—all methods use most fields. In a low-cohesion class, you'll see isolated clusters—groups of methods that never interact with each other's fields.
Many static analysis tools calculate LCOM and related metrics: SonarQube, NDepend, JArchitect, and IDE plugins. Use these to identify low-cohesion classes as refactoring candidates. However, don't blindly follow metrics—use them as signals that warrant investigation, not as absolute rules.
Certain class patterns almost always indicate cohesion problems. Learning to recognize these anti-patterns helps you spot SRP violations early:
StringUtils.capitalize(), MathUtils.round(), DateUtils.format() all in one place. Pure coincidental cohesion—a home for the homeless.UserManager might handle authentication, profiles, preferences, and notifications—four distinct responsibilities.initialize(), start(), pause(), resume(), stop(), cleanup() for completely unrelated subsystems.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Anti-Pattern: The God Class// This class has LOW cohesion - methods use disjoint field setsclass OrderService { // Pricing fields private taxCalculator: TaxCalculator; private discountEngine: DiscountEngine; private currencyConverter: CurrencyConverter; // Persistence fields private orderRepository: OrderRepository; private productRepository: ProductRepository; // Notification fields private emailService: EmailService; private smsService: SmsService; private pushNotifier: PushNotifier; // Reporting fields private reportGenerator: ReportGenerator; private analyticsClient: AnalyticsClient; // Pricing methods (use pricing fields) calculateOrderTotal(order: Order): Money { /* ... */ } applyDiscount(order: Order, code: string): void { /* ... */ } convertCurrency(amount: Money, to: Currency): Money { /* ... */ } // Persistence methods (use persistence fields) saveOrder(order: Order): void { /* ... */ } findOrder(id: string): Order { /* ... */ } updateOrderStatus(id: string, status: Status): void { /* ... */ } // Notification methods (use notification fields) sendOrderConfirmation(order: Order): void { /* ... */ } notifyShippingUpdate(order: Order, tracking: string): void { /* ... */ } sendAbandonedCartReminder(cart: Cart): void { /* ... */ } // Reporting methods (use reporting fields) generateDailySalesReport(): Report { /* ... */ } trackConversion(order: Order): void { /* ... */ } exportToAnalytics(orders: Order[]): void { /* ... */ }} // This class should be split into 4+ smaller classes,// each with high cohesion and a single responsibility:// - OrderPricing// - OrderRepository // - OrderNotifications// - OrderAnalyticsHow to recognize these patterns:
When you identify a low-cohesion class, several refactoring strategies can help:
Strategy 1: Extract Class
Identify a group of methods and fields that form a natural cluster. Extract them into a new class with a focused responsibility.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// BEFORE: Low cohesion - payment processing mixed with order logicclass Order { private items: OrderItem[]; private customerId: string; // Payment fields - candidates for extraction private cardNumber: string; private cardExpiry: Date; private cardCvv: string; private billingAddress: Address; addItem(item: OrderItem): void { /* ... */ } removeItem(itemId: string): void { /* ... */ } calculateTotal(): Money { /* ... */ } // Payment methods - candidates for extraction validateCard(): boolean { /* ... */ } chargeCard(amount: Money): PaymentResult { /* ... */ } refund(amount: Money): PaymentResult { /* ... */ }} // AFTER: Extract PaymentDetails classclass Order { private items: OrderItem[]; private customerId: string; private payment: PaymentDetails; // Composition addItem(item: OrderItem): void { /* ... */ } removeItem(itemId: string): void { /* ... */ } calculateTotal(): Money { /* ... */ } processPayment(): PaymentResult { return this.payment.charge(this.calculateTotal()); }} class PaymentDetails { private cardNumber: string; private cardExpiry: Date; private cardCvv: string; private billingAddress: Address; validate(): boolean { /* ... */ } charge(amount: Money): PaymentResult { /* ... */ } refund(amount: Money): PaymentResult { /* ... */ }}Strategy 2: Extract Interface
When a class serves multiple clients with different needs, extract interfaces representing each client's view. This doesn't improve cohesion directly but clarifies the different roles the class plays.
Strategy 3: Split by Lifecycle
If a class has methods for creation, operation, and teardown that use different fields, consider splitting into lifecycle-specific classes.
Strategy 4: Use Composition
Replace a God Class with a coordinator that delegates to specialized collaborators. The coordinator becomes thin—just orchestrating calls to cohesive subordinates.
Don't try to fix all cohesion problems at once. Extract one class at a time, ensure tests pass, and stabilize before continuing. Large-scale refactoring is risky; incremental improvement is sustainable.
Cohesion and coupling are related but distinct concepts:
The goal: High cohesion, low coupling.
But there's a tension: increasing cohesion by splitting classes can increase coupling between the resulting classes. Finding the right balance is a design skill.
When splitting increases coupling acceptably:
When splitting increases coupling unacceptably:
| Situation | Cohesion Impact | Coupling Impact | Recommendation |
|---|---|---|---|
| Extract truly independent responsibility | ↑ Both classes more focused | Minimal coupling added | ✓ Split |
| Extract data class (DTO) | ↑ Behavior class focused | Data coupling (acceptable) | ✓ Split if reused |
| Extract helper with shared state | ↔ Marginal improvement | ↑ Tight coupling | ✗ Keep together or redesign |
| Split forces circular dependencies | ↑ Local cohesion | ↑ Coupling (cyclic) | ✗ Redesign responsibility boundaries |
| Methods always change together | ↔ No real improvement | ↑ Coordination overhead | ✗ Don't split |
The "Together or Independent" Test:
Ask yourself: "If these elements were in separate classes, would they change independently?"
The goal isn't maximum cohesion at all costs. It's finding natural boundaries where the resulting classes are both internally cohesive and externally loosely coupled.
Cohesion provides the positive lens for evaluating SRP compliance. Let's consolidate the key insights:
What's next:
We've explored what constitutes a responsibility (actor-based definition) and how to measure good design (cohesion). The final piece is identifying common responsibility boundaries—the typical ways responsibilities are divided in real-world systems. This practical catalog will accelerate your ability to apply SRP effectively.
You now understand cohesion as the positive measure of well-designed responsibilities. Combined with actor-based thinking, cohesion analysis gives you powerful tools for evaluating and improving class design. Next, we'll explore common responsibility boundary patterns in real-world systems.