Skip to content

SRP in Practice

When to Refactor Toward SRP

Refactor when you experience: - Tests that require mocking unrelated dependencies - Changes in one feature breaking unrelated features - Difficulty naming a class without "and" or "Manager" - Classes exceeding ~200-300 lines (soft threshold)

Pattern: Detecting Violations

Code Smell Checklist: 1. Count reasons to change -- if > 1, split 2. List dependencies -- unrelated deps indicate mixed concerns 3. Check test setup -- excessive mocking suggests too many responsibilities 4. Review method names -- unrelated verbs indicate low cohesion

Python Example:

# Violation: mixed data access and business logic
class Invoice:
    def calculate_total(self):
        return sum(item.price for item in self.items)

    def save_to_database(self):
        db.execute("INSERT INTO invoices ...")

    def generate_pdf(self):
        return PDF(self.data)

# SRP-Compliant
class Invoice:
    def calculate_total(self):
        return sum(item.price for item in self.items)

class InvoiceRepository:
    def save(self, invoice: Invoice):
        db.execute("INSERT INTO invoices ...")

class InvoicePDFGenerator:
    def generate(self, invoice: Invoice) -> PDF:
        return PDF(invoice.data)

Refactoring Strategy

Step 1: Identify Responsibilities List everything the class does. Group related actions.

Step 2: Extract by Responsibility Create new classes for each group. Use Extract Class refactoring.

Step 3: Update Dependencies Replace direct calls with dependency injection.

Step 4: Test in Isolation Each new class should have focused unit tests.

Decision: When NOT to Split

Scenario Action Why
Class is stable and rarely changes Keep together Premature abstraction adds complexity
Responsibilities are tightly coupled Keep together Splitting creates awkward dependencies
Class is < 50 lines and cohesive Keep together Over-engineering hurts readability
Team is unfamiliar with domain Keep together initially Split when patterns emerge

Common Mistakes

  • Extracting too early -- Wait for actual change pressure before splitting. Why: YAGNI violation, speculative complexity
  • Creating circular dependencies -- If A needs B and B needs A after splitting, you split wrong. Why: indicates incorrect responsibility boundaries
  • Splitting data and behavior -- Keep related data and methods together (rich domain models). Why: anemic models push logic elsewhere
  • One class per method -- SRP doesn't mean "one public method". Why: over-granularity destroys cohesion
  • Ignoring the axis of change -- Split along actual change axes, not arbitrary groupings. Why: doesn't solve the maintenance problem

Performance Considerations

Myth: SRP hurts performance due to object proliferation. Reality: Modern compilers/runtimes optimize object creation. Maintainability gains outweigh micro-optimizations.

When performance matters: Profile first. If SRP-compliant design is a bottleneck (rare), document the trade-off and optimize locally.