2.2.4 · D4Design Principles

Exercises — SOLID — Single Responsibility Principle

2,608 words12 min readBack to topic

Before we begin, one shared picture of the whole ladder:

Figure — SOLID — Single Responsibility Principle

Level 1 — Recognition

Goal: can you spot how many actors a class serves, without changing anything yet?

Exercise 1.1 (L1)

Here is a class. List its methods, and for each, name the likely actor (the role that would ask to change it).

class Invoice {
    double computeTotal()      // adds line items + tax
    String renderHtml()        // makes a web page of the invoice
    void   persistToDatabase() // writes the invoice to storage
}

How many reasons to change does this class have?

Recall Solution 1.1
  • computeTotal()Accounting / Finance (they own tax and pricing rules).
  • renderHtml()Web/UI team (they own how it looks).
  • persistToDatabase()DBA / persistence team (they own storage format).

Three distinct actors ⇒ 3 reasons to change. This class violates SRP: a pricing tweak, a layout tweak, and a schema tweak all land in the same file.

Exercise 1.2 (L1)

True or false: "A class with 8 methods automatically violates SRP because 8 > 1." Explain in one sentence.

Recall Solution 1.2

False. SRP counts reasons to change (actors), not number of methods. If all 8 methods change only when one actor (say Accounting) changes its rules, the class has one reason to change and is SRP-clean.


Level 2 — Application

Goal: given a violation, perform the split.

Exercise 2.1 (L2)

Refactor the Invoice class from 1.1 into SRP-clean pieces. Name the classes and say what data is shared. Where does the passive data go?

Recall Solution 2.1

Separate data from policies, one class per actor (see the parent's recipe):

class InvoiceData {          // passive: changes only if the data model changes
    List<LineItem> items; double taxRate;
}
class InvoiceCalculator { double computeTotal(InvoiceData d){...} }  // Accounting
class InvoiceRenderer   { String renderHtml(InvoiceData d){...} }    // Web/UI
class InvoiceRepository { void persist(InvoiceData d){...} }         // DBA
  • Shared data lives in InvoiceData (passive, no policy).
  • Each policy class serves exactly one actor ⇒ each has .
  • See Cohesion and Coupling: we raised cohesion (one reason per class) and cut coupling between the three actors.

Exercise 2.2 (L2)

After the split in 2.1, a client complains: "Now I have to construct three objects to print an invoice — it used to be one call!" What pattern restores a single entry point, and does it re-violate SRP?

Recall Solution 2.2

Add a Facade:

class InvoiceService {              // convenience entry point
    InvoiceCalculator calc; InvoiceRenderer render; InvoiceRepository repo;
    void printAndSave(InvoiceData d){ ... delegates ... }
}

It does not re-violate SRP: the Facade's only reason to change is "the way we wire these three together" (orchestration). It holds no pricing, layout, or storage logic — it merely delegates.


Level 3 — Analysis

Goal: reason about trade-offs and put numbers on risk.

Exercise 3.1 (L3)

Using the parent's cross-actor risk formula compute the risk for a class serving actors with per-pair break probability . Show each step and interpret the number.

Recall Solution 3.1
  • other actors that could be broken.
  • Survival per other actor .
  • All three survive (independent) .
  • At least one broken .

Meaning: ~38.6% of change requests risk breaking an unrelated actor. Splitting into four classes ( each) drives to .

Exercise 3.2 (L3)

Two designs handle 4 actors:

  • Design G (god-class): one class, , .
  • Design S (split): four classes, each .

By how many percentage points does splitting reduce cross-actor risk? Reference the figure.

Figure — SOLID — Single Responsibility Principle
Recall Solution 3.2
  • Design G: (from 3.1).
  • Design S: .
  • Reduction percentage points.

The red curve in the figure climbs steeply with ; splitting slides you back to the flat point where the curve touches zero.

Exercise 3.3 (L3)

DRY tension: a junior notices InvoiceCalculator and InvoiceRenderer both format currency ($1,234.50). They extract a shared formatMoney() helper called by both. Is this an SRP problem? Analyse.

Recall Solution 3.3

It depends on who owns the format. Currency formatting is presentation, owned by the Web/UI actor — so it belongs with the renderer's concern, not the calculator's. Letting the calculator depend on it re-couples Accounting to UI.

  • If both truly share it, put formatMoney() in a neutral utility owned by neither business actor (a formatting/i18n actor), so a currency-display change has one owner.
  • Blind DRY that merges code across actors recreates the exact cross-actor coupling SRP forbids (see the parent's regularHours() example).

Level 4 — Synthesis

Goal: design a system and derive/justify choices from first principles.

Exercise 4.1 (L4)

You are designing an Order subsystem for an e-commerce app. Requirements arrive from: Sales (discounts), Warehouse (stock reservation), Finance (tax/receipts), and Notifications (emails). Design the classes so each actor has exactly one reason to change, plus a single client entry point. Justify each boundary.

Recall Solution 4.1
class OrderData { List<Item> items; Customer customer; }   // passive schema
 
class DiscountPolicy   { Money applyDiscounts(OrderData o){...} }  // Sales
class StockReserver    { void  reserve(OrderData o){...} }         // Warehouse
class TaxAndReceipt    { Receipt finalize(OrderData o){...} }      // Finance
class OrderNotifier    { void  notifyCustomer(OrderData o){...} }  // Notifications
 
class CheckoutService {   // Facade: only reason to change = orchestration order
    DiscountPolicy d; StockReserver s; TaxAndReceipt t; OrderNotifier n;
    Receipt checkout(OrderData o){ /* delegate in sequence */ }
}

Justifications (one reason each):

  • DiscountPolicy changes ⇔ Sales changes promo rules. Nobody else pulls it.
  • StockReserver changes ⇔ Warehouse changes reservation logic.
  • TaxAndReceipt changes ⇔ Finance changes tax law / receipt format.
  • OrderNotifier changes ⇔ Notifications changes email/SMS wording.
  • OrderData changes ⇔ the schema changes (one, data-only reason).
  • CheckoutService changes ⇔ the sequence of steps changes — pure wiring.

Each policy class has . This design also opens the door to extending behaviour (new discount type) without editing others.

Exercise 4.2 (L4)

Extend 4.1: Sales now wants three discount strategies (seasonal, loyalty, bulk), switchable at runtime. Show how to add them without editing the other classes, and name which principle this leans on beyond SRP.

Recall Solution 4.2

Make DiscountPolicy an interface (an abstraction) and inject the chosen one:

interface DiscountPolicy { Money applyDiscounts(OrderData o); }
class SeasonalDiscount implements DiscountPolicy {...}
class LoyaltyDiscount  implements DiscountPolicy {...}
class BulkDiscount     implements DiscountPolicy {...}

CheckoutService depends on the interface, not a concrete class, so new strategies plug in with zero edits to CheckoutService, StockReserver, etc.

  • This is open for extension, closed for modification.
  • Depending on the abstraction (interface) rather than the concretions is Dependency Inversion. SRP gave clean seams; these two principles exploit those seams.

Level 5 — Mastery

Goal: defend subtle edge and degenerate cases like an expert.

Exercise 5.1 (L5)

Two teams currently agree on identical logging code, so a class serves what looks like one actor. Later the security team asks for audit logging while the ops team keeps debug logging. Prove that "how many actors?" is time-dependent, and state the safe design rule that survives this drift.

Recall Solution 5.1
  • At time : logging serves both teams identically → looks like .
  • At time : security wants audit trails, ops wants debug volume → the same code now has two divergent reasons to change, and .
  • Proof of time-dependence: the count of actors changed while the code did not; therefore is a property of requirement sources over time, not of the current text.
  • Safe rule: design for anticipated axes of change. When two roles could diverge, keep the seam (separate class / interface) even if they agree today. A cheap seam now beats a painful shotgun-surgery split later.

Exercise 5.2 (L5 — degenerate case)

Evaluate the risk formula at the boundaries: , , and and for general . Interpret each degenerate value.

Recall Solution 5.2

Using :

  • (no actor uses the class): exponent , . This is mathematically defined but physically meaningless — a class serving nobody has no change requests at all, so the model's domain is . Treat as dead code: delete it. Risk is undefined-because-irrelevant.
  • : . SRP-clean: no other actor exists to break.
  • , any : . If changes never leak (, perfect isolation), coupling costs nothing — but real shared state rarely gives .
  • , : . Every change breaks another actor for certain — the worst god-class.

Exercise 5.3 (L5)

A reviewer says: "You split too far — you now have 12 one-method classes and every feature touches five files. That's worse!" Reconcile this with SRP: was SRP wrong, or was it misapplied? Give the diagnostic question.

Recall Solution 5.3

Misapplied, not wrong. Splitting by method instead of by actor creates a maze of anaemic classes and shotgun surgery — one logical change ripples across many files because one actor's concern got scattered.

  • Diagnostic question: "When actor X requests a change, how many files must I edit?"
    • Ideal SRP: exactly one file (that actor's class).
    • Under-split (god-class): one file, but it also breaks other actors ().
    • Over-split: many files for one actor → shotgun surgery.
  • The sweet spot is one class per actor — the same actor's methods stay together (high cohesion), different actors stay apart.

Recall Feynman check: teach it back

If you can answer these in your own words, you've mastered D4. What is a "reason to change"? ::: One actor — a single role/stakeholder that requests changes. Why is a Facade not an SRP violation? ::: Its only reason to change is orchestration/wiring; it owns no business policy. Why can blind DRY re-violate SRP? ::: It merges code that changes for different reasons, re-coupling two actors via one helper. What is when ? ::: — no other actor exists to accidentally break. Over-splitting causes which smell? ::: Shotgun surgery — one change touches many files.


Connections

  • SOLID — Single Responsibility Principle — the parent principle these exercises drill.
  • Cohesion and Coupling — SRP = high cohesion (one reason) + low cross-actor coupling.
  • Facade Pattern — the single entry point after splitting (Ex 2.2, 4.1).
  • Open-Closed Principle — clean seams let you extend without editing (Ex 4.2).
  • Dependency Inversion Principle — depend on abstractions across those seams (Ex 4.2).
  • God Object (anti-pattern) — the under-split failure mode (Ex 4.2 trap).
  • Shotgun Surgery — the over-split failure mode (Ex 5.3).
  • DRY Principle — its tension with SRP (Ex 3.3).