Loading content...
"This code feels messy" is a common observation. "This module seems to do too much" captures an intuition. But when presenting a refactoring proposal to stakeholders, when prioritizing technical debt, or when evaluating architectural options, intuition alone isn't sufficient. You need data.
Measuring cohesion and coupling transforms subjective assessments into objective metrics. Instead of arguing about whether code is "well-designed," you can point to specific measurements: "This class has a LCOM value of 0.87, indicating very low cohesion—only 13% of methods share common attributes." Or: "These two modules have 47 coupling points, while the architectural standard is under 10."
Metrics don't replace judgment, but they inform it. They reveal patterns invisible to code review, track trends over time, and provide early warnings before problems become crises.
This page equips you with quantitative tools for assessing cohesion and coupling. You will learn established metrics like LCOM and coupling measures, understand their calculation and interpretation, explore practical heuristics for quick assessment, and discover tools that automate measurement.
Before diving into specific metrics, let's establish why measurement matters and what we hope to achieve through quantification.
Goodhart's Law states: 'When a measure becomes a target, it ceases to be a good measure.' Don't optimize for metrics at the expense of actual design quality. A class can technically achieve perfect cohesion scores while still being poorly designed. Use metrics as indicators, not objectives.
The most established cohesion metrics belong to the LCOM (Lack of Cohesion in Methods) family. Originally proposed by Chidamber and Kemerer in their influential 1994 metrics suite, LCOM has evolved through several variations, each addressing limitations of its predecessors.
LCOM1: The Original Formulation
LCOM1 counts pairs of methods that share no instance variables versus pairs that share at least one:
LCOM1 = |P| - |Q|, if |P| > |Q|
LCOM1 = 0, otherwise
Where:
- P = pairs of methods with no shared instance variables
- Q = pairs of methods with at least one shared instance variable
Interpretation: Higher values indicate lower cohesion. A value of 0 suggests high cohesion (all method pairs share data). Positive values indicate methods that don't share data—potential cohesion problems.
Limitations: LCOM1 has several issues:
LCOM2 and LCOM3: Improved Formulations
LCOM2 and LCOM3 address some LCOM1 shortcomings by changing the calculation approach.
LCOM2 (Henderson-Sellers):
LCOM2 = (m - sum(a) / f) / (m - 1)
Where:
- m = number of methods
- f = number of instance variables (fields)
- a = number of methods accessing each field
This produces a value between 0 and 1:
LCOM3 refines this further by considering method-to-method connections through shared variable access, producing a graph-based cohesion measure.
| Variant | Range | Interpretation | Best For |
|---|---|---|---|
| LCOM1 | 0 to ∞ | 0 = high cohesion; higher = lower cohesion | Historical reference; rarely used now |
| LCOM2 | 0 to 1 | 0 = perfect cohesion; 1 = no cohesion | Normalized comparison across classes |
| LCOM3 | 0 to ∞ | Number of connected components in method graph | Identifying split candidates |
| LCOM4 | 1 to m | Connected components; 1 = cohesive; >1 = splittable | Refactoring guidance |
| LCOM5 | 0 to 1 | Similar to LCOM2 with method call consideration | Modern codebases |
12345678910111213141516171819202122232425262728293031323334353637383940414243
/** * Example class for LCOM2 calculation */class Rectangle { private width: number; // Field 1 private height: number; // Field 2 // Method 1: uses width and height getArea(): number { return this.width * this.height; } // Method 2: uses width and height getPerimeter(): number { return 2 * (this.width + this.height); } // Method 3: uses width only getWidth(): number { return this.width; } // Method 4: uses height only getHeight(): number { return this.height; }} /*LCOM2 Calculation:- m (methods) = 4- f (fields) = 2- Methods accessing 'width': getArea, getPerimeter, getWidth = 3- Methods accessing 'height': getArea, getPerimeter, getHeight = 3- sum(a) = 3 + 3 = 6- Average methods per field = 6 / 2 = 3 LCOM2 = (4 - 3) / (4 - 1) = 1/3 ≈ 0.33 Interpretation: Moderate cohesion. Not perfect (some methods don't use all fields),but reasonably cohesive (most methods use most fields).*/LCOM4 is particularly useful for identifying split opportunities. It counts the number of connected components in a graph where methods are connected if they share a variable. A class with LCOM4 > 1 can potentially be split into that many separate classes, each being a connected component.
Coupling metrics quantify the dependencies between modules. Several established metrics address different aspects of coupling, from simple counts to more nuanced stability measures.
CBO (Coupling Between Objects)
CBO counts the number of classes to which a given class is coupled. A class is coupled to another if it uses methods or instance variables of that class.
CBO = Count of classes that this class depends on
+ Count of classes that depend on this class
Interpretation:
Thresholds: While context-dependent, research suggests CBO > 14 correlates with increased defect probability.
Afferent and Efferent Coupling (Ca and Ce)
Robert C. Martin's metrics distinguish incoming and outgoing dependencies:
Why both matter:
Both should be monitored, but they have different implications for stability and change.
Instability Metric (I)
Combining Ca and Ce yields the Instability metric:
I = Ce / (Ca + Ce)
Interpretation:
The Stable Dependencies Principle (SDP): Dependencies should flow toward stability. Unstable modules (I → 1) should depend on stable modules (I → 0), not the reverse. If a stable module depends on an unstable one, you've inverted the stability gradient and created fragility.
| Metric | What It Measures | Healthy Range | Action When High |
|---|---|---|---|
| CBO | Total classes coupled to this class | < 10-15 | Extract responsibilities, introduce abstractions |
| Ca (Afferent) | Incoming dependencies (who depends on me) | Context-dependent | Ensure stability; changes are risky |
| Ce (Efferent) | Outgoing dependencies (who do I depend on) | < 10-20 | Reduce dependencies, apply DIP |
| Instability (I) | Ratio of outgoing to total coupling | Varies by role | Ensure SDP compliance |
| RFC (Response for Class) | Methods callable in response to message | < 50 | Reduce method count, extract classes |
Martin also defines Abstractness (A) as the ratio of abstract classes/interfaces to total classes in a package. Combined with Instability, this allows plotting packages on an A-I graph. Packages should lie along the 'main sequence' (A + I ≈ 1). Deviation indicates either 'rigid' (stable but concrete) or 'useless' (unstable but abstract) packages.
Formal metrics are valuable for systematic analysis, but everyday development often requires quick heuristics—rules of thumb that indicate cohesion or coupling problems without calculation.
// User Authentication, // User Preferences, // User Analytics sections within one class, each section probably deserves its own class.Quick cohesion smell check:
class UserController {
// Group A: Authentication
private authService: AuthService;
private tokenManager: TokenManager;
login() { }
logout() { }
// Group B: Profile (different fields!)
private profileRepo: ProfileRepository;
private imageProcessor: ImageProcessor;
updateProfile() { }
uploadAvatar() { }
// Group C: Settings (again different!)
private settingsRepo: SettingsRepository;
getSettings() { }
updateSettings() { }
}
Three distinct field groups = three classes hiding inside one.
Quick coupling smell check:
// 15+ imports = coupling smell
import { Database } from './db';
import { Cache } from './cache';
import { Logger } from './logger';
import { Metrics } from './metrics';
import { Auth } from './auth';
import { Email } from './email';
import { SMS } from './sms';
import { Push } from './push';
import { Queue } from './queue';
import { Storage } from './storage';
import { Config } from './config';
import { Validator } from './validator';
import { Transformer } from './transformer';
import { Formatter } from './formatter';
import { Analytics } from './analytics';
// ... more imports
This class knows too many other classes.
Heuristics are fast; metrics are precise. Use heuristics for daily code review and initial assessment. Use metrics for systematic codebase analysis, trend tracking, and threshold enforcement in CI pipelines.
Numbers tell part of the story; visualizations reveal patterns that raw metrics obscure. Several visualization techniques help identify cohesion and coupling issues at a glance.
Dependency Graphs
The most direct visualization of coupling is a dependency graph where nodes are modules and edges are dependencies.
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Orders │────▶│ Users │◀────│ Auth │
└────┬────┘ └────┬────┘ └─────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Payment │────▶│ Notify │
└─────────┘ └─────────┘
What to look for:
Dependency Structure Matrix (DSM)
A DSM is a square matrix where both rows and columns represent modules. Cell (i, j) indicates whether module i depends on module j.
Users Orders Payment Auth Notify
Users - . . X .
Orders X - X . X
Payment . . - . X
Auth X . . - .
Notify . . . . -
Benefits:
Heatmaps for Cohesion
A method-field heatmap visualizes which methods use which fields within a class:
Field1 Field2 Field3 Field4 Field5
getArea() ████ ████ ░░░░ ░░░░ ░░░░
getPerimeter() ████ ████ ░░░░ ░░░░ ░░░░
getWidth() ████ ░░░░ ░░░░ ░░░░ ░░░░
getHeight() ░░░░ ████ ░░░░ ░░░░ ░░░░
sendEmail() ░░░░ ░░░░ ████ ████ ░░░░
logAnalytics() ░░░░ ░░░░ ░░░░ ░░░░ ████
The distinct blocks (getArea/getPerimeter using Fields 1-2 vs sendEmail using Fields 3-4) reveal that this class contains at least two unrelated cohesive groups. The LCOM4 for this class would be 3, suggesting split into three classes.
Many static analysis tools generate these visualizations automatically. Examples include: Structure101, NDepend (.NET), JDepend (Java), Dependency Cruiser (JavaScript/TypeScript), and Madge (JavaScript). IDE plugins often provide dependency diagrams. Invest in tooling for regular codebase health checks.
Manual measurement doesn't scale. For ongoing codebase health monitoring, automated tools are essential. Here's a survey of tools across different ecosystems.
| Ecosystem | Tool | Key Features |
|---|---|---|
| JavaScript/TypeScript | ESLint + plugins | Custom rules for import limits, dependency restrictions |
| JavaScript/TypeScript | Madge | Dependency graphs, circular dependency detection |
| JavaScript/TypeScript | Dependency Cruiser | Configurable dependency validation, graph generation |
| Java | JDepend | Package-level metrics (Ca, Ce, I, A, D) |
| Java | ArchUnit | Architecture tests as code, layer validation |
| .NET | NDepend | Comprehensive metrics, CQLinq queries, trend analysis |
| Multi-language | SonarQube | Code quality metrics, dashboard, CI integration |
| Multi-language | Structure101 | Architecture visualization, dependency DSM |
| Multi-language | CodeScene | Behavioral analysis, hotspot detection, team coupling |
Integrating measurement into CI/CD:
The most effective use of metrics is automated enforcement through your build pipeline:
# Example: Dependency cruiser in CI
- name: Check Dependencies
run: |
npx depcruise --validate .dependency-cruiser.js src/
# .dependency-cruiser.js
module.exports = {
forbidden: [
{
name: 'no-circular',
severity: 'error',
from: {},
to: { circular: true }
},
{
name: 'no-cross-layer',
comment: 'Domain should not depend on infrastructure',
severity: 'error',
from: { path: '^src/domain' },
to: { path: '^src/infrastructure' }
},
{
name: 'max-module-dependencies',
severity: 'warn',
from: {},
to: {},
module: { numberOfDependenciesLessThan: 15 }
}
]
};
This catches coupling violations before code merges, maintaining architectural integrity automatically.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
import * as fs from 'fs';import * as path from 'path'; interface ModuleMetrics { name: string; imports: string[]; importCount: number; // Ce (efferent) importedBy: string[]; importedByCount: number; // Ca (afferent) instability: number; // I = Ce / (Ce + Ca)} function analyzeModule(filePath: string): ModuleMetrics { const content = fs.readFileSync(filePath, 'utf-8'); const importRegex = /import .* from ['"](.*)['"]/g; const imports: string[] = []; let match; while ((match = importRegex.exec(content)) !== null) { imports.push(match[1]); } return { name: filePath, imports, importCount: imports.length, importedBy: [], // Populated in second pass importedByCount: 0, instability: 0, // Calculated after second pass };} function calculateInstability(metrics: Map<string, ModuleMetrics>): void { // Second pass: populate importedBy from imports metrics.forEach((module) => { module.imports.forEach((importPath) => { const imported = metrics.get(importPath); if (imported) { imported.importedBy.push(module.name); imported.importedByCount++; } }); }); // Calculate instability metrics.forEach((module) => { const ce = module.importCount; // Outgoing const ca = module.importedByCount; // Incoming module.instability = ca + ce > 0 ? ce / (ca + ce) : 0; });} // Usage: node analyze.js ./src// Produces coupling report for all modulesYou don't need sophisticated tooling to start measuring. Begin with import counting and basic threshold checks. Add more sophisticated metrics as your needs grow. The key is to start measuring consistently, not to measure everything perfectly from day one.
Metrics without interpretation are just numbers. Understanding what metrics mean in context—and what actions they suggest—transforms data into design improvement.
| Metric Pattern | What It Indicates | Recommended Actions |
|---|---|---|
| High LCOM (> 0.6) | Methods don't share data; class combines unrelated concerns | Identify method clusters; extract into separate classes |
| High CBO (> 15) | Class has too many dependencies | Introduce abstractions; apply DIP; extract mediator |
| High Ce, low Ca | Unstable module depending on many others | Appropriate for leaf/peripheral modules; verify no stable modules depend on it |
| Low Ce, high Ca | Stable core module; many dependents | Ensure high abstractness; avoid concrete implementation in stable modules |
| Circular dependencies | Modules form cycles; cannot be built/tested independently | Break cycle with interface extraction; use dependency injection |
| Rising metrics trend | Architecture is degrading over time | Prioritize refactoring; add CI thresholds to prevent further degradation |
Context matters:
Metric thresholds aren't universal. What's acceptable depends on:
Establish thresholds appropriate for your context, then enforce them consistently.
Prioritizing refactoring based on metrics:
Not all metric violations deserve equal attention. Prioritize based on:
Combine these factors into a prioritization score. Attack the highest-scoring modules first.
Perfect metrics are not the goal. 'Good enough' cohesion and coupling that enable the team to work effectively is the goal. Spending weeks achieving perfect LCOM scores on rarely-changing utility classes is likely waste. Focus measurement and improvement on modules where it matters.
We've explored how to quantify cohesion and coupling—moving from intuition to measurement. Let's consolidate the key insights:
What's next:
We've now covered cohesion, coupling, and measurement. The final page explores the trade-offs in design—the tensions and balancing acts required when cohesion competes with other goals, when reducing coupling introduces its own costs, and when pragmatic considerations trump metric optimization.
You now have a quantitative toolkit for assessing cohesion and coupling. You understand established metrics, can apply practical heuristics, know how to visualize dependency structures, and can automate measurement in CI pipelines. Next, we'll explore the inevitable trade-offs in applying these concepts.