The Single Responsibility Principle (SRP) says a class should have only one reason to change. Put differently, each class does one job well. When a class mixes business logic, storage, emailing and tax rules, a change to any of them risks breaking the others.
The idea#
Think of kitchen appliances: a toaster toasts, a blender blends, a microwave heats. One machine that tried to do all three would be complicated and break easily.
A quick test: describe the class in one sentence. If you need the word "and" ("it stores employee data and saves to the database and sends email"), it probably has more than one responsibility.
"Reason to change" usually maps to a person or team who would ask for the change: HR changes pay rules, the DBA changes storage, marketing changes email templates.
Violation#
This Employee class does four unrelated jobs:
class Employee:
def __init__(self, name: str, salary: float):
self._name = name
self._salary = salary
def work(self) -> None: # employee behavior
print(f"{self._name} is working")
def save_to_database(self) -> None: # persistence
print("Saving to database...")
def send_email(self) -> None: # notifications
print("Sending email...")
def calculate_tax(self) -> None: # tax rules
tax = self._salary * 0.2
print(f"Tax: {tax}")
Employee("Ana", 50_000).calculate_tax() # Tax: 10000.0
Four responsibilities, so four different reasons this one file might change.
Fix#
Split each responsibility into its own class:
class Employee:
def __init__(self, name: str, salary: float):
self._name = name
self._salary = salary
def work(self) -> None:
print(f"{self._name} is working")
@property
def salary(self) -> float:
return self._salary
class EmployeeRepository:
def save(self, emp: Employee) -> None:
print("Saving employee to database...")
class EmailService:
def send_email(self, to: str, message: str) -> None:
print(f"Sending email to {to}")
class TaxCalculator:
def calculate_tax(self, salary: float) -> float:
return salary * 0.2
ana = Employee("Ana", 50_000)
EmployeeRepository().save(ana)
print(TaxCalculator().calculate_tax(ana.salary)) # 10000.0
Each class now has one clear job and can be changed and tested in isolation.
Trade-offs#
SRP can be overdone:
- Splitting too aggressively creates many tiny classes that are hard to navigate. Group by reason to change, not by method count.
- SRP doesn't forbid a class having several methods; they just need to serve the same purpose.
Key takeaways#
- One class, one responsibility, one reason to change.
- The "and" test catches most violations.
- Move persistence, notification and calculation logic into their own classes.
- Smaller, focused classes are easier to test and safer to modify.