🎯 The SRP Rule
“A class should have only ONE reason to change.”
Think of it like workers in a restaurant:
👨🍳 Chef - Only cooks food
🍽️ Waiter - Only serves customers
💰 Cashier - Only handles payments
🧹 Cleaner - Only cleans tables
If the chef also had to serve, clean, AND handle money, one bad day could mess up EVERYTHING!
Simple Rule : If you describe your class and use the word “AND”, you might be breaking SRP!❌ “This class saves users AND sends emails AND logs activity”
✅ “This class saves users”
🚨 Spotting SRP Violations
❌ BAD: The God Class
class UserManager :
"""This class does EVERYTHING related to users... and more!"""
def __init__ ( self , db_connection ):
self .db = db_connection
# Responsibility 1: User data operations
def create_user ( self , name , email , password ):
hashed_pw = self ._hash_password(password)
self .db.execute(
"INSERT INTO users VALUES (?, ?, ?)" ,
(name, email, hashed_pw)
)
def get_user ( self , user_id ):
return self .db.query( f "SELECT * FROM users WHERE id = { user_id } " )
def update_user ( self , user_id , data ):
# ... database update logic
pass
# Responsibility 2: Authentication (DIFFERENT JOB!)
def _hash_password ( self , password ):
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
def verify_password ( self , user_id , password ):
user = self .get_user(user_id)
return user[ 'password' ] == self ._hash_password(password)
# Responsibility 3: Email sending (DIFFERENT JOB!)
def send_welcome_email ( self , user_email ):
import smtplib
# Complex email sending logic...
server = smtplib.SMTP( 'smtp.gmail.com' , 587 )
server.send_message( f "Welcome to our app!" )
def send_password_reset ( self , user_email ):
# More email logic...
pass
# Responsibility 4: Logging (DIFFERENT JOB!)
def log_activity ( self , user_id , action ):
with open ( 'user_activity.log' , 'a' ) as f:
f.write( f " { user_id } : { action } \n " )
# Responsibility 5: Report generation (DIFFERENT JOB!)
def generate_user_report ( self ):
users = self .db.query( "SELECT * FROM users" )
return f "Total users: { len (users) } "
# 😱 This class has 5 reasons to change:
# 1. If database schema changes
# 2. If password hashing algorithm changes
# 3. If email provider changes
# 4. If logging format changes
# 5. If report format changes
✅ GOOD: Focused Classes
# Each class has ONE job!
class UserRepository :
"""ONLY handles user data storage"""
def __init__ ( self , db_connection ):
self .db = db_connection
def create ( self , user_data ):
self .db.execute( "INSERT INTO users ..." , user_data)
def find_by_id ( self , user_id ):
return self .db.query( "SELECT * FROM users WHERE id = ?" , user_id)
def update ( self , user_id , data ):
self .db.execute( "UPDATE users SET ..." , data)
def delete ( self , user_id ):
self .db.execute( "DELETE FROM users WHERE id = ?" , user_id)
class PasswordService :
"""ONLY handles password operations"""
def hash ( self , password ):
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
def verify ( self , password , hashed ):
return self .hash(password) == hashed
class EmailService :
"""ONLY handles email sending"""
def __init__ ( self , smtp_config ):
self .config = smtp_config
def send ( self , to , subject , body ):
# All email logic here
print ( f "📧 Sending ' { subject } ' to { to } " )
def send_welcome ( self , user_email ):
self .send(user_email, "Welcome!" , "Thanks for joining!" )
def send_password_reset ( self , user_email , reset_link ):
self .send(user_email, "Reset Password" , f "Click: { reset_link } " )
class ActivityLogger :
"""ONLY handles logging"""
def __init__ ( self , log_file ):
self .log_file = log_file
def log ( self , user_id , action ):
with open ( self .log_file, 'a' ) as f:
from datetime import datetime
timestamp = datetime.now().isoformat()
f.write( f "[ { timestamp } ] User { user_id } : { action } \n " )
class UserReportGenerator :
"""ONLY generates reports"""
def __init__ ( self , user_repository ):
self .repo = user_repository
def generate_summary ( self ):
users = self .repo.find_all()
return {
"total" : len (users),
"active" : sum ( 1 for u in users if u.is_active)
}
# 🎉 Now each class has ONE reason to change!
# The UserService coordinates them:
class UserService :
"""Coordinates user operations using focused services"""
def __init__ ( self ):
self .user_repo = UserRepository(db_connection)
self .password_service = PasswordService()
self .email_service = EmailService(smtp_config)
self .logger = ActivityLogger( 'activity.log' )
def register_user ( self , name , email , password ):
# Use each service for its specific job
hashed_pw = self .password_service.hash(password)
user = self .user_repo.create({ 'name' : name, 'email' : email, 'password' : hashed_pw})
self .email_service.send_welcome(email)
self .logger.log(user.id, 'registered' )
return user
🎮 Real Example: Invoice System
Let’s fix an invoice system step by step:
❌ Before: One Class Does Everything
class Invoice :
def __init__ ( self , items ):
self .items = items
# Responsibility 1: Calculate totals
def calculate_total ( self ):
return sum (item.price * item.quantity for item in self .items)
def calculate_tax ( self ):
return self .calculate_total() * 0.1
# Responsibility 2: Format/Print (DIFFERENT JOB!)
def print_invoice ( self ):
print ( "=" * 40 )
print ( "INVOICE" )
print ( "=" * 40 )
for item in self .items:
print ( f " { item.name } : $ { item.price } x { item.quantity } " )
print ( f "Total: $ { self .calculate_total() } " )
print ( f "Tax: $ { self .calculate_tax() } " )
def export_to_pdf ( self ):
# PDF generation logic...
pass
def export_to_excel ( self ):
# Excel generation logic...
pass
# Responsibility 3: Save to database (DIFFERENT JOB!)
def save_to_database ( self , db ):
db.execute( "INSERT INTO invoices ..." )
# Responsibility 4: Send via email (DIFFERENT JOB!)
def email_to_customer ( self , email ):
# Email sending logic...
pass
✅ After: Each Class Has One Job
from dataclasses import dataclass
from typing import List
@dataclass
class InvoiceItem :
name: str
price: float
quantity: int
class Invoice :
"""ONLY holds invoice data and calculations"""
def __init__ ( self , invoice_id : str , items : List[InvoiceItem]):
self .id = invoice_id
self .items = items
def calculate_subtotal ( self ):
return sum (item.price * item.quantity for item in self .items)
def calculate_tax ( self , tax_rate = 0.1 ):
return self .calculate_subtotal() * tax_rate
def calculate_total ( self , tax_rate = 0.1 ):
return self .calculate_subtotal() + self .calculate_tax(tax_rate)
class InvoicePrinter :
"""ONLY handles printing"""
def print_to_console ( self , invoice : Invoice):
print ( "╔════════════════════════════════════════╗" )
print ( f "║ INVOICE # { invoice.id } " )
print ( "╠════════════════════════════════════════╣" )
for item in invoice.items:
line = f " { item.name } : $ { item.price :.2f} x { item.quantity } "
print ( f "║ { line :<39} ║" )
print ( "╠════════════════════════════════════════╣" )
print ( f "║ Subtotal: $ { invoice.calculate_subtotal() :>26.2f} ║" )
print ( f "║ Tax: $ { invoice.calculate_tax() :>26.2f} ║" )
print ( f "║ TOTAL: $ { invoice.calculate_total() :>26.2f} ║" )
print ( "╚════════════════════════════════════════╝" )
class InvoiceExporter :
"""ONLY handles exporting to different formats"""
def to_pdf ( self , invoice : Invoice, filename : str ):
print ( f "📄 Exporting invoice { invoice.id } to { filename } .pdf" )
# PDF generation logic
def to_excel ( self , invoice : Invoice, filename : str ):
print ( f "📊 Exporting invoice { invoice.id } to { filename } .xlsx" )
# Excel generation logic
def to_json ( self , invoice : Invoice) -> str :
import json
return json.dumps({
'id' : invoice.id,
'items' : [ vars (item) for item in invoice.items],
'total' : invoice.calculate_total()
})
class InvoiceRepository :
"""ONLY handles database operations"""
def __init__ ( self , database ):
self .db = database
def save ( self , invoice : Invoice):
print ( f "💾 Saving invoice { invoice.id } to database" )
# Database save logic
def find_by_id ( self , invoice_id : str ) -> Invoice:
print ( f "🔍 Finding invoice { invoice_id } " )
# Database query logic
pass
class InvoiceEmailer :
"""ONLY handles email sending"""
def __init__ ( self , email_service ):
self .email_service = email_service
def send_to_customer ( self , invoice : Invoice, customer_email : str ):
print ( f "📧 Emailing invoice { invoice.id } to { customer_email } " )
# Email sending logic
# 🎉 Usage - clear separation of concerns!
items = [
InvoiceItem( "Widget" , 10.00 , 5 ),
InvoiceItem( "Gadget" , 25.00 , 2 ),
]
invoice = Invoice( "INV-001" , items)
# Each service does its ONE job
printer = InvoicePrinter()
printer.print_to_console(invoice)
exporter = InvoiceExporter()
exporter.to_pdf(invoice, "invoice_001" )
# Easy to swap, test, or modify each piece!
🧪 How to Check for SRP Violations
Use these questions:
Can you describe the class in one sentence WITHOUT using “and”?
✅ “This class stores user data”
❌ “This class stores user data AND sends emails”
If you need to change one feature, do you touch multiple unrelated methods?
✅ Changing email format only touches EmailService
❌ Changing email format requires editing UserManager
How many reasons could this class change?
✅ One reason (e.g., business rules for orders)
❌ Multiple reasons (email format, database schema, logging format…)
Could you easily test this class in isolation?
✅ UserRepository can be tested with just a mock database
❌ UserManager needs mock database, mock email server, mock logger…
Would a team member understand the class purpose immediately?
✅ “InvoicePrinter” - obviously prints invoices
❌ “InvoiceManager” - does it manage? print? email? save?
💡 Common SRP Violations & Fixes
Violation Problem Fix God Class Does everything Split into focused classes Utility Class Bag of random methods Group by purpose into services Active Record Data + Database + Validation Separate into Entity + Repository Fat Controller HTTP + Business Logic + Data Use Service Layer pattern
🏋️ Practice Exercise
Challenge: Fix the BookStore Class
This class violates SRP. Split it into focused classes: class BookStore :
def __init__ ( self ):
self .books = []
self .customers = []
self .orders = []
# Data operations
def add_book ( self , book ): pass
def remove_book ( self , book_id ): pass
def find_book ( self , title ): pass
# Customer operations
def register_customer ( self , customer ): pass
def update_customer ( self , customer_id , data ): pass
# Order operations
def create_order ( self , customer_id , book_ids ): pass
def cancel_order ( self , order_id ): pass
# Inventory
def check_stock ( self , book_id ): pass
def restock ( self , book_id , quantity ): pass
# Reports
def generate_sales_report ( self ): pass
def generate_inventory_report ( self ): pass
# Notifications
def send_order_confirmation ( self , order_id ): pass
def send_shipping_update ( self , order_id , status ): pass
📝 Key Takeaways
One Job Only Each class should have only ONE reason to change
No 'AND' Description If you use “and” to describe it, split it up
Easy to Test Focused classes are easy to test in isolation
Easy to Name Good names describe exactly what the class does
🏃 Next: Open/Closed Principle
Now that your classes are focused, let’s learn how to add features WITHOUT changing existing code!
Continue to Open/Closed Principle → Learn how to extend your code without modifying it!