Single Responsibility
One Invoice class calculates the total, prints the text and saves to the database.
Split by reason to change.
A class mixes maths, display and storage (or any two unrelated jobs)
The picture from life
In a small restaurant the cook cooks and the cashier handles the bill. If one person did both, a change in the menu and a change in the tax rule would both land on the same person, and a mistake in one would spoil the other. Give each job to its own person and a change touches only one of them.
The problem
What goes wrong when you do not use it.
One Invoice class calculates the total, prints the text and saves to the database. Change the print layout and you risk breaking the maths. Change the database and you reopen the printing code. Every request touches the same fragile file.
- calls / uses
- A class mixes maths, display and storage (or any two unrelated jobs)
- You say "and" when describing what the class does
- A small change makes you retest unrelated features
The idea
How the pattern fixes it.
Split by reason to change. Ask "who would ask me to change this?": the finance team (maths), the designer (layout), the DBA (storage). Each answer becomes its own small class.
- calls / uses
Who is who
- Invoice
- Holds items and knows the total. Changes only when pricing rules change.
- InvoicePrinter
- Turns an invoice into text. Changes only when the layout changes.
- InvoiceStore
- Saves invoices. Changes only when storage changes.
Build it in steps
- 1List what the class does in plain words.
- 2Group the jobs by who would ask for a change.
- 3Move each group into its own class.
- 4Let a small caller wire them together.
Watch it run
Step through the calls. Tap an arrow to jump to it.
Storing is the store's job. The invoice does not know how it is saved.
save(invoice)The code
Python and Java. Try writing the classes yourself from the diagram first.
class Invoice:
def __init__(self, items):
self.items = items # list of (name, price)
def total(self):
return sum(price for _, price in self.items)
class InvoicePrinter:
def text(self, invoice):
names = " | ".join(name for name, _ in invoice.items)
return f"{names} | TOTAL {invoice.total()}"
class InvoiceStore:
def __init__(self):
self.saved = []
def save(self, invoice):
self.saved.append(invoice)
def demo():
invoice = Invoice([("pen", 10), ("book", 90)])
store = InvoiceStore()
store.save(invoice)
return InvoicePrinter().text(invoice), len(store.saved)What you gain
- A change stays inside one small class
- Each class is easy to test alone
- Teams can work on different classes without conflicts
What it costs
- More classes and files to navigate
- Over-splitting a tiny class into many pieces just adds noise
You have already met it in
- A Controller / Service / Repository split in web frameworks
- Separating a logger from the code that logs
Often mixed up with
- FacadeFacade hides many classes behind one door; SRP is about splitting one overloaded class apart.
Make it stick
Take a class you wrote that both calculates and prints. Split it in two and make the printer swappable for a new format.