SOLID I: Single Responsibility & Open/Closed

Clean Code, SOLID & Refactoring

Chapter 6 · SOLID I: Single Responsibility & Open/Closed

SOLID gives names to disciplines this course has already been practicing informally. Chapter 4's Large Class and Chapter 5's Feature Envy were both early instances of the first principle here; Chapter 1's platinum-tier example was an early instance of the second. This chapter verifies both principles directly, at their sharpest.

Single Responsibility: "One Reason to Change," Tested Concretely

# BAD — one class, two responsibilities: calculating pay AND formatting a report class PayrollReportBad: def __init__(self, employees): self.employees = employees def calculate_total_pay(self): return sum(e['hours'] * e['rate'] for e in self.employees) def print_report(self): total = self.calculate_total_pay() # the formatter depends directly on the calculator return f"Total: ${total:.2f}"
Verified directly — the report formatter genuinely cannot be tested while the calculation is broken
Simulating calculate_total_pay temporarily broken (a real, common mid-refactor state): calling print_report() — which is supposed to be testing formatting, nothing else — correctly fails with RuntimeError: pay calculation logic is broken mid-refactor. There is no way to verify the report's own formatting is correct while this class's other responsibility is broken, because the two are bundled into one object with no boundary between them.
# GOOD — split by responsibility class PayCalculator: def calculate_total_pay(self): # ... class PayrollReportFormatter: def format_report(self, total): return f"Total: ${total:.2f}"
Verified directly — the formatter now tests correctly with zero dependency on the calculator
Calling PayrollReportFormatter().format_report(9999.99) — a fake total, with PayCalculator never even constructed — correctly returns "Total: $9999.99". And PayCalculator's own source, captured via inspect.getsource() before and after using the formatter: byte-identical, True. The two responsibilities can now genuinely be verified, changed, and reasoned about independently.
This is the same shape as Software Architecture Fundamentals Chapter 7's own fake adapters
A formatter that can be tested against a fake total, with no working calculator required, is the exact same testability property that chapter measured as an ~18,111× speedup at a larger scale. SRP is what makes a component small and self-contained enough to fake in the first place.

Open/Closed: Extended Without Touching a Single Existing Line

class DiscountStrategy: def apply(self, price): raise NotImplementedError class NoDiscount(DiscountStrategy): def apply(self, price): return price class GoldDiscount(DiscountStrategy): def apply(self, price): return price * 0.8 class PricingEngine: def __init__(self, strategy): self.strategy = strategy def calculate(self, price): return self.strategy.apply(price)
Verified directly — every existing class stayed byte-identical after a real extension
Adding a new PlatinumDiscount(DiscountStrategy) class — a genuine new discount type — and re-capturing every existing class's own source afterward: PricingEngine unchanged (True), DiscountStrategy unchanged (True), NoDiscount unchanged (True), GoldDiscount unchanged (True). Nothing that already existed was opened, edited, or even needed to be re-read.
Verified directly — the completely unmodified PricingEngine correctly runs the new discount
PricingEngine(PlatinumDiscount()).calculate(100) correctly returns 70.0PricingEngine's own code never changed, yet it correctly applies a discount type that didn't exist when it was written. This is "closed for modification" verified literally, not just "unlikely to need a change."
This is Design Patterns' own Strategy, one course later, formally named
Design Patterns Chapter 7 built this exact shape and verified swapping strategies at runtime. This chapter's own finding — that PricingEngine stays genuinely untouched by new discount types — is what Open/Closed means: Strategy (and, for a different kind of extension, Decorator, Design Patterns Chapter 5) are concrete design patterns that exist specifically to satisfy this principle. SOLID names the goal; the patterns are working implementations of it.

Where This Connects

This chapter's findingWhat it connects to
A bundled class's own formatter untestable while the calculator is brokenChapter 4's own Large Class finding (3 concerns in 1 class) — the exact same problem, now measured by testability instead of concern-counting
4 existing classes verified byte-identical after a real extensionChapter 1's own platinum-tier example — the identical principle, verified more completely (whole classes, not one dict line)
Strategy named directly as an OCP implementationDesign Patterns Chapter 5's Decorator — a second, equally valid way to satisfy Open/Closed, covered next in Chapter 8's refactoring catalog

Hands-On Exercises

Exercise 1

Add a second method to PayrollReportFormatter, format_summary_line(total, employee_count), following the same pattern as format_report. Verify it can be tested with two fake values, with zero dependency on PayCalculator, exactly like this chapter's own format_report.

📄 View solution
Exercise 2

Add a second new discount type, SilverDiscount (15% off), to this chapter's own DiscountStrategy hierarchy. Verify every one of the now-five existing classes (including PlatinumDiscount from this chapter) stays byte-identical, and verify PricingEngine correctly applies it.

📄 View solution
Exercise 3

Using this chapter's own two verified findings, explain why satisfying Open/Closed for DiscountStrategy required Single Responsibility to already be true for PricingEngine — what would have gone wrong extending PricingEngine with a new discount type if it had been bundled with unrelated responsibilities the way PayrollReportBad was?

📄 View solution

Chapter 6 Quick Reference

  • Single Responsibility, verified: a bundled formatter genuinely couldn't be tested while its class's other responsibility was broken; a split-out formatter tested correctly against a fake value with zero dependency
  • Open/Closed, verified: all 4 existing classes stayed byte-identical after a real extension, with the completely unmodified PricingEngine correctly applying the new discount
  • The connection to Design Patterns: Strategy and Decorator are concrete, working implementations of Open/Closed — this chapter measured the principle; that course built the mechanism
  • Next chapter: SOLID II — Liskov Substitution, Interface Segregation & Dependency Inversion