Exercises — SOLID — Single Responsibility Principle
Before we begin, one shared picture of the whole ladder:

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.

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):
DiscountPolicychanges ⇔ Sales changes promo rules. Nobody else pulls it.StockReserverchanges ⇔ Warehouse changes reservation logic.TaxAndReceiptchanges ⇔ Finance changes tax law / receipt format.OrderNotifierchanges ⇔ Notifications changes email/SMS wording.OrderDatachanges ⇔ the schema changes (one, data-only reason).CheckoutServicechanges ⇔ 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).