S · SOLID
Single Responsibility Principle
A class should have only one reason to change. When a class juggles unrelated jobs, a change to one job risks breaking the others.
✗ The problem
One class, three jobs
This Report computes totals, formats HTML, and saves to disk.
Click a request below — watch which lines it forces you to touch.
class Report {
calculateTotals(rows) { /* business logic */ }
renderHtml(totals) { /* presentation */ }
saveToDisk(html) { /* persistence */ }
}
Report
calc · format · save
Three unrelated triggers — finance, design, infra — all reopen the
same class. A tweak to the HTML can break totals. That's the smell.
✓ The refactor
Split by responsibility
Give each job its own class. Each now has exactly one reason to change.
// class = the role (noun) · method = the action (verb)
class TotalsCalculator { compute(rows) {…} }
class ReportRenderer { toHtml(totals) {…} }
class FileStore { save(path, data) {…} }
TotalsCalculator
business logic
ReportRenderer
presentation
FileStore
persistence
Methods don't repeat the class name (
Calculator.calculate
"stutters"). A clean verb — compute / toHtml / save —
reads like a swappable contract: the caller cares what it does, not what it's called.
✓ Compose, don't cram
↓
↓
A thin coordinator wires them together
The pieces stay independent; a small function orchestrates the flow.
function buildReport(rows) {
const totals = calculator.compute(rows);
const html = renderer.toHtml(totals);
store.save('report.html', html);
}
TotalsCalculator
ReportRenderer
FileStore
Now a rebrand touches only
ReportRenderer. A storage change touches
only FileStore. Nothing else moves.
✓ Takeaway
Spot it, split it
- Smell: the word "and" in a class description ("formats and saves").
- Test: can two different stakeholders demand changes to the same class? If yes, split.
- Benefit: smaller blast radius, easier tests, safer reuse.
- Careful: don't over-split — cohesive logic that changes together should stay together.
Next: Open/Closed → — extend behavior without editing old code.