Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

Single Responsibility Principle (SRP) Tutorial 🎯

Table of Contents

  1. Introduction
  2. What is the Single Responsibility Principle?
  3. Understanding "Responsibility"
  4. Why SRP Matters
  5. Common Violations and Code Smells
  6. Basic Examples
  7. Intermediate Examples
  8. Advanced Examples
  9. Real-World Applications
  10. Best Practices
  11. Common Pitfalls
  12. Exercises
  13. Summary

Introduction

Welcome to the comprehensive tutorial on the Single Responsibility Principle (SRP) - the first and arguably most important principle in the SOLID design principles. Whether you're a beginner just starting with object-oriented programming or an experienced developer looking to refine your design skills, this tutorial will guide you through understanding and applying SRP effectively.

What You'll Learn

  • 🎯 Core Concept: What SRP means and why it exists
  • 🔍 Identification: How to spot SRP violations in code
  • 🛠️ Application: How to refactor code to follow SRP
  • 💡 Best Practices: Professional techniques for maintaining SRP
  • 🚀 Real Examples: Practical scenarios from beginner to expert level

Prerequisites

  • Basic understanding of classes and objects
  • Familiarity with Python syntax
  • Understanding of methods and attributes

What is the Single Responsibility Principle?

"A class should have only one reason to change." - Robert C. Martin

The Single Responsibility Principle states that a class should have only one job or responsibility. This means that a class should only have one reason to be modified.

Simple Analogy 🏠

Think of SRP like organizing your home:

  • Kitchen → Cooking and food preparation
  • Bedroom → Sleeping and rest
  • Office → Work and study
  • Bathroom → Personal hygiene

Each room has a single, clear purpose. You wouldn't cook in your bedroom or sleep in your kitchen because it would be confusing and inefficient. The same principle applies to classes in programming.

Technical Definition

A class adheres to SRP when:

  1. It has one primary responsibility
  2. It has one reason to change
  3. All its methods and attributes support that single responsibility

Understanding "Responsibility"

What Constitutes a Responsibility?

A responsibility is a reason for change. To identify responsibilities, ask yourself:

  1. "What does this class do?" - List all the actions
  2. "Why would this class need to change?" - List all the reasons
  3. "Who would request changes to this class?" - Identify stakeholders

Examples of Different Responsibilities

Responsibility Type Examples Typical Changes
Data Management Store, retrieve, validate data Database schema changes, validation rules
Business Logic Calculate, process, transform Business rule changes, algorithm updates
User Interface Display, format, present UI design changes, layout modifications
Communication Send emails, API calls, logging Protocol changes, service endpoints
Configuration Settings, preferences, options Configuration format, default values

🎯 Key Insight

If you can describe a class with "AND" instead of a single verb, it likely violates SRP:

  • ❌ "This class manages users AND sends emails AND validates data"
  • ✅ "This class manages users"

Why SRP Matters

Benefits of Following SRP

1. Easier Maintenance 🔧

# ❌ Hard to maintain - multiple responsibilities
class UserManager:
    def save_user(self, user): pass
    def send_welcome_email(self, user): pass
    def validate_email(self, email): pass
    def generate_report(self, users): pass

# ✅ Easy to maintain - single responsibility
class UserRepository:
    def save_user(self, user): pass
    def get_user(self, user_id): pass

2. Better Testability 🧪

  • Focused tests: Each class has a clear testing scope
  • Isolated testing: Changes in one area don't break unrelated tests
  • Simpler mocking: Fewer dependencies to mock

3. Improved Readability 📖

  • Clear purpose: Anyone can understand what the class does
  • Predictable behavior: Methods relate to the class's main responsibility
  • Better naming: Class names accurately reflect their purpose

4. Reduced Coupling 🔗

  • Independent changes: Modify one responsibility without affecting others
  • Flexible design: Easy to swap implementations
  • Parallel development: Teams can work on different responsibilities

5. Enhanced Reusability ♻️

  • Focused functionality: Classes can be reused in different contexts
  • Smaller interfaces: Easier to integrate with other systems
  • Modular design: Components can be combined in various ways

Real-World Impact

Before SRP (Problems)

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def calculate_pay(self):
        # Business logic responsibility
        return self.salary * 12
    
    def save_to_database(self):
        # Data persistence responsibility
        # Database connection code...
        pass
    
    def generate_report(self):
        # Reporting responsibility
        return f"Employee: {self.name}, Annual Pay: {self.calculate_pay()}"

Problems with this approach:

  • 🚫 Database changes affect the Employee class
  • 🚫 Report format changes affect the Employee class
  • 🚫 Pay calculation changes affect the Employee class
  • 🚫 Hard to test each responsibility independently
  • 🚫 Multiple teams might need to modify the same class

After SRP (Solutions)

class Employee:
    """Responsible for: Employee data representation"""
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

class PayCalculator:
    """Responsible for: Pay calculation logic"""
    def calculate_annual_pay(self, employee):
        return employee.salary * 12

class EmployeeRepository:
    """Responsible for: Employee data persistence"""
    def save(self, employee):
        # Database operations
        pass

class EmployeeReportGenerator:
    """Responsible for: Employee reporting"""
    def __init__(self, pay_calculator):
        self.pay_calculator = pay_calculator
    
    def generate_report(self, employee):
        annual_pay = self.pay_calculator.calculate_annual_pay(employee)
        return f"Employee: {employee.name}, Annual Pay: {annual_pay}"

Benefits of this approach:

  • Independent changes: Each class can evolve separately
  • Easy testing: Test each responsibility in isolation
  • Team collaboration: Different teams can work on different classes
  • Reusability: PayCalculator can be used for different employee types
  • Clear ownership: Each class has a clear purpose and owner

Common Violations and Code Smells

How to Identify SRP Violations

🚨 Warning Signs

  1. Large Classes (>200-300 lines)
  2. Many Methods (>10-15 methods)
  3. Multiple Import Statements from different domains
  4. Complex Constructor with many parameters
  5. Mixed Abstraction Levels in the same class
  6. Frequent Changes to the same class for different reasons

🔍 Code Smell Examples

1. The "God Class"
# ❌ Violates SRP - Too many responsibilities
class UserManager:
    def __init__(self):
        self.database = Database()
        self.email_service = EmailService()
        self.logger = Logger()
        self.validator = Validator()
    
    # User data management
    def create_user(self, user_data): pass
    def update_user(self, user_id, data): pass
    def delete_user(self, user_id): pass
    
    # Authentication
    def login(self, username, password): pass
    def logout(self, user_id): pass
    def reset_password(self, email): pass
    
    # Email operations
    def send_welcome_email(self, user): pass
    def send_notification(self, user, message): pass
    
    # Reporting
    def generate_user_report(self): pass
    def export_users_csv(self): pass
    
    # Validation
    def validate_email(self, email): pass
    def validate_password(self, password): pass
2. Mixed Abstraction Levels
# ❌ Violates SRP - Mixing high-level and low-level operations
class OrderProcessor:
    def process_order(self, order):
        # High-level business logic
        if self.validate_order(order):
            # Low-level database operations
            connection = sqlite3.connect('orders.db')
            cursor = connection.cursor()
            cursor.execute("INSERT INTO orders...", order.data)
            connection.commit()
            
            # Low-level email operations
            smtp_server = smtplib.SMTP('smtp.gmail.com', 587)
            smtp_server.starttls()
            smtp_server.login(username, password)
            # ... email sending code
3. Multiple Reasons to Change
# ❌ Violates SRP - Changes for different stakeholders
class Invoice:
    def calculate_total(self):
        # Finance team might change this
        pass
    
    def print_invoice(self):
        # UI/UX team might change this
        pass
    
    def save_to_database(self):
        # Database team might change this
        pass
    
    def send_by_email(self):
        # IT/Infrastructure team might change this
        pass

Refactoring Strategies

Strategy 1: Extract Classes

# ✅ Separate responsibilities into different classes
class User:
    """Data representation only"""
    def __init__(self, username, email):
        self.username = username
        self.email = email

class UserRepository:
    """Data persistence only"""
    def save(self, user): pass
    def find_by_id(self, user_id): pass

class UserAuthenticator:
    """Authentication only"""
    def login(self, username, password): pass
    def logout(self, user): pass

class EmailService:
    """Email operations only"""
    def send_welcome_email(self, user): pass

Strategy 2: Use Composition

# ✅ Compose services for complex operations
class UserService:
    def __init__(self, repository, authenticator, email_service):
        self.repository = repository
        self.authenticator = authenticator
        self.email_service = email_service
    
    def register_user(self, user_data):
        user = User(user_data['username'], user_data['email'])
        self.repository.save(user)
        self.email_service.send_welcome_email(user)
        return user

Basic Examples

Example 1: Simple User Management

Before: SRP Violation

class User:
    def __init__(self, username, email, password):
        self.username = username
        self.email = email
        self.password = password
    
    def save_to_file(self):
        """Responsibility: File I/O"""
        with open('users.txt', 'a') as f:
            f.write(f"{self.username},{self.email},{self.password}\n")
    
    def send_welcome_email(self):
        """Responsibility: Email communication"""
        print(f"Sending welcome email to {self.email}")
    
    def validate_email(self):
        """Responsibility: Data validation"""
        return '@' in self.email and '.' in self.email
    
    def hash_password(self):
        """Responsibility: Security/Encryption"""
        import hashlib
        return hashlib.md5(self.password.encode()).hexdigest()

Problems:

  • User class has 4 different responsibilities
  • Changes to file format affect User class
  • Changes to email service affect User class
  • Changes to validation rules affect User class
  • Hard to test each responsibility independently

After: Following SRP

class User:
    """Responsibility: User data representation"""
    def __init__(self, username, email, password):
        self.username = username
        self.email = email
        self.password = password

class UserRepository:
    """Responsibility: User data persistence"""
    def save_to_file(self, user):
        with open('users.txt', 'a') as f:
            f.write(f"{user.username},{user.email},{user.password}\n")
    
    def load_from_file(self):
        users = []
        try:
            with open('users.txt', 'r') as f:
                for line in f:
                    username, email, password = line.strip().split(',')
                    users.append(User(username, email, password))
        except FileNotFoundError:
            pass
        return users

class EmailService:
    """Responsibility: Email communication"""
    def send_welcome_email(self, user):
        print(f"Sending welcome email to {user.email}")
        # In real implementation: SMTP, templates, etc.

class UserValidator:
    """Responsibility: User data validation"""
    def validate_email(self, email):
        return '@' in email and '.' in email
    
    def validate_password(self, password):
        return len(password) >= 8

class PasswordHasher:
    """Responsibility: Password security"""
    def hash_password(self, password):
        import hashlib
        return hashlib.sha256(password.encode()).hexdigest()

# Usage example
class UserService:
    """Orchestrates user operations using single-responsibility classes"""
    def __init__(self):
        self.repository = UserRepository()
        self.email_service = EmailService()
        self.validator = UserValidator()
        self.hasher = PasswordHasher()
    
    def register_user(self, username, email, password):
        # Validate input
        if not self.validator.validate_email(email):
            raise ValueError("Invalid email")
        if not self.validator.validate_password(password):
            raise ValueError("Password too weak")
        
        # Create user with hashed password
        hashed_password = self.hasher.hash_password(password)
        user = User(username, email, hashed_password)
        
        # Save and notify
        self.repository.save_to_file(user)
        self.email_service.send_welcome_email(user)
        
        return user

Benefits:

  • ✅ Each class has a single, clear responsibility
  • ✅ Easy to test each component independently
  • ✅ Changes to file format only affect UserRepository
  • ✅ Changes to email service only affect EmailService
  • ✅ Easy to swap implementations (file → database, email → SMS)
  • ✅ Code is more readable and maintainable

Intermediate Examples

Example 2: E-commerce Order System

Before: Complex SRP Violation

class Order:
    def __init__(self, customer_id, items):
        self.customer_id = customer_id
        self.items = items
        self.status = "pending"
        self.total = 0
    
    def calculate_total(self):
        """Responsibility: Business logic - pricing"""
        self.total = sum(item['price'] * item['quantity'] for item in self.items)
        # Apply discounts
        if self.total > 100:
            self.total *= 0.9  # 10% discount
        return self.total
    
    def validate_order(self):
        """Responsibility: Business logic - validation"""
        if not self.items:
            return False
        for item in self.items:
            if item['quantity'] <= 0:
                return False
        return True
    
    def save_to_database(self):
        """Responsibility: Data persistence"""
        import sqlite3
        conn = sqlite3.connect('orders.db')
        cursor = conn.cursor()
        cursor.execute("""
            INSERT INTO orders (customer_id, total, status) 
            VALUES (?, ?, ?)
        """, (self.customer_id, self.total, self.status))
        conn.commit()
        conn.close()
    
    def send_confirmation_email(self):
        """Responsibility: Communication"""
        print(f"Sending order confirmation email to customer {self.customer_id}")
        # Email implementation...
    
    def generate_invoice_pdf(self):
        """Responsibility: Document generation"""
        print(f"Generating PDF invoice for order {self.customer_id}")
        # PDF generation logic...
    
    def process_payment(self, payment_method):
        """Responsibility: Payment processing"""
        if payment_method == "credit_card":
            print("Processing credit card payment...")
        elif payment_method == "paypal":
            print("Processing PayPal payment...")
        return True
    
    def update_inventory(self):
        """Responsibility: Inventory management"""
        for item in self.items:
            print(f"Reducing inventory for {item['name']} by {item['quantity']}")

After: Proper SRP Implementation

# Data Models
class Order:
    """Responsibility: Order data representation"""
    def __init__(self, customer_id, items):
        self.customer_id = customer_id
        self.items = items
        self.status = "pending"
        self.total = 0
        self.created_at = datetime.now()

class OrderItem:
    """Responsibility: Order item data representation"""
    def __init__(self, product_id, name, price, quantity):
        self.product_id = product_id
        self.name = name
        self.price = price
        self.quantity = quantity

# Business Logic
class PricingCalculator:
    """Responsibility: Price calculations and discounts"""
    def calculate_order_total(self, order):
        subtotal = sum(item.price * item.quantity for item in order.items)
        return self.apply_discounts(subtotal, order)
    
    def apply_discounts(self, subtotal, order):
        if subtotal > 100:
            return subtotal * 0.9  # 10% discount
        return subtotal

class OrderValidator:
    """Responsibility: Order validation logic"""
    def validate_order(self, order):
        if not order.items:
            raise ValueError("Order must contain at least one item")
        
        for item in order.items:
            if item.quantity <= 0:
                raise ValueError(f"Invalid quantity for {item.name}")
        
        return True

# Data Persistence
class OrderRepository:
    """Responsibility: Order data persistence"""
    def save(self, order):
        import sqlite3
        conn = sqlite3.connect('orders.db')
        cursor = conn.cursor()
        cursor.execute("""
            INSERT INTO orders (customer_id, total, status, created_at) 
            VALUES (?, ?, ?, ?)
        """, (order.customer_id, order.total, order.status, order.created_at))
        order.id = cursor.lastrowid
        conn.commit()
        conn.close()
    
    def find_by_id(self, order_id):
        # Implementation to retrieve order
        pass

# Communication
class NotificationService:
    """Responsibility: Customer notifications"""
    def send_order_confirmation(self, order, customer_email):
        print(f"Sending order confirmation email to {customer_email}")
        # Email implementation...

# Document Generation
class InvoiceGenerator:
    """Responsibility: Invoice document creation"""
    def generate_pdf(self, order):
        print(f"Generating PDF invoice for order {order.id}")
        # PDF generation logic...

# Payment Processing
class PaymentProcessor:
    """Responsibility: Payment handling"""
    def process_payment(self, order, payment_method, payment_details):
        if payment_method == "credit_card":
            return self._process_credit_card(payment_details)
        elif payment_method == "paypal":
            return self._process_paypal(payment_details)
        else:
            raise ValueError("Unsupported payment method")
    
    def _process_credit_card(self, details):
        print("Processing credit card payment...")
        return {"status": "success", "transaction_id": "cc_123"}
    
    def _process_paypal(self, details):
        print("Processing PayPal payment...")
        return {"status": "success", "transaction_id": "pp_456"}

# Inventory Management
class InventoryManager:
    """Responsibility: Inventory updates"""
    def reserve_items(self, order):
        for item in order.items:
            print(f"Reserving {item.quantity} units of {item.name}")
    
    def release_items(self, order):
        for item in order.items:
            print(f"Releasing {item.quantity} units of {item.name}")

# Orchestration Service
class OrderService:
    """Responsibility: Orchestrating order processing workflow"""
    def __init__(self):
        self.validator = OrderValidator()
        self.pricing_calculator = PricingCalculator()
        self.repository = OrderRepository()
        self.notification_service = NotificationService()
        self.invoice_generator = InvoiceGenerator()
        self.payment_processor = PaymentProcessor()
        self.inventory_manager = InventoryManager()
    
    def process_order(self, order, payment_method, payment_details, customer_email):
        try:
            # Validate order
            self.validator.validate_order(order)
            
            # Calculate pricing
            order.total = self.pricing_calculator.calculate_order_total(order)
            
            # Reserve inventory
            self.inventory_manager.reserve_items(order)
            
            # Process payment
            payment_result = self.payment_processor.process_payment(
                order, payment_method, payment_details
            )
            
            if payment_result["status"] == "success":
                # Save order
                order.status = "confirmed"
                self.repository.save(order)
                
                # Send notifications
                self.notification_service.send_order_confirmation(order, customer_email)
                
                # Generate invoice
                self.invoice_generator.generate_pdf(order)
                
                return order
            else:
                # Release inventory on payment failure
                self.inventory_manager.release_items(order)
                raise Exception("Payment failed")
                
        except Exception as e:
            order.status = "failed"
            self.inventory_manager.release_items(order)
            raise e

Key Improvements:

  • 8 separate classes each with a single responsibility
  • Easy testing: Each component can be tested independently
  • Flexible payment: Easy to add new payment methods
  • Swappable storage: Can change from SQLite to PostgreSQL
  • Independent changes: Pricing changes don't affect email logic
  • Clear ownership: Each team can own specific components

Advanced Examples

Example 3: Content Management System

Real-World Scenario: Blog Platform

# Domain Models
class Article:
    """Responsibility: Article data representation"""
    def __init__(self, title, content, author_id, category_id):
        self.id = None
        self.title = title
        self.content = content
        self.author_id = author_id
        self.category_id = category_id
        self.status = "draft"
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
        self.view_count = 0

class Author:
    """Responsibility: Author data representation"""
    def __init__(self, name, email, bio=""):
        self.id = None
        self.name = name
        self.email = email
        self.bio = bio

# Content Processing
class ContentProcessor:
    """Responsibility: Content formatting and processing"""
    def process_markdown(self, content):
        # Convert markdown to HTML
        return content.replace("**", "<strong>").replace("**", "</strong>")
    
    def extract_summary(self, content, max_length=200):
        if len(content) <= max_length:
            return content
        return content[:max_length] + "..."
    
    def extract_tags(self, content):
        # Extract hashtags from content
        import re
        return re.findall(r'#(\w+)', content)

class ContentValidator:
    """Responsibility: Content validation rules"""
    def validate_article(self, article):
        errors = []
        
        if not article.title or len(article.title.strip()) < 5:
            errors.append("Title must be at least 5 characters")
        
        if not article.content or len(article.content.strip()) < 50:
            errors.append("Content must be at least 50 characters")
        
        if self._contains_prohibited_words(article.content):
            errors.append("Content contains prohibited words")
        
        return errors
    
    def _contains_prohibited_words(self, content):
        prohibited = ["spam", "fake", "scam"]
        return any(word in content.lower() for word in prohibited)

# Data Access Layer
class ArticleRepository:
    """Responsibility: Article data persistence"""
    def save(self, article):
        # Database save logic
        pass
    
    def find_by_id(self, article_id):
        # Database retrieval logic
        pass
    
    def find_by_author(self, author_id):
        # Find articles by author
        pass
    
    def find_published(self, limit=10, offset=0):
        # Find published articles with pagination
        pass

class AuthorRepository:
    """Responsibility: Author data persistence"""
    def save(self, author):
        pass
    
    def find_by_email(self, email):
        pass

# Search and Analytics
class SearchEngine:
    """Responsibility: Article search functionality"""
    def search_articles(self, query, filters=None):
        # Implement search logic (could use Elasticsearch, etc.)
        pass
    
    def get_related_articles(self, article, limit=5):
        # Find related articles based on tags, category, etc.
        pass

class AnalyticsTracker:
    """Responsibility: Usage analytics and metrics"""
    def track_article_view(self, article_id, user_id=None):
        # Track article views
        pass
    
    def get_popular_articles(self, time_period="week"):
        # Get most viewed articles
        pass
    
    def get_author_stats(self, author_id):
        # Get author performance metrics
        pass

# Caching Layer
class CacheManager:
    """Responsibility: Content caching"""
    def get_cached_article(self, article_id):
        # Get article from cache (Redis, Memcached, etc.)
        pass
    
    def cache_article(self, article):
        # Cache article for faster access
        pass
    
    def invalidate_cache(self, article_id):
        # Remove from cache when updated
        pass

# Notification System
class NotificationService:
    """Responsibility: User notifications"""
    def notify_new_article(self, article, subscribers):
        # Notify subscribers of new article
        pass
    
    def notify_article_published(self, article):
        # Notify author when article is published
        pass

# Publishing Workflow
class PublishingWorkflow:
    """Responsibility: Article publishing process"""
    def __init__(self):
        self.states = ["draft", "review", "approved", "published", "archived"]
    
    def can_transition(self, from_state, to_state):
        transitions = {
            "draft": ["review"],
            "review": ["draft", "approved"],
            "approved": ["published"],
            "published": ["archived"]
        }
        return to_state in transitions.get(from_state, [])
    
    def transition_article(self, article, new_state, user_role):
        if not self.can_transition(article.status, new_state):
            raise ValueError(f"Cannot transition from {article.status} to {new_state}")
        
        if new_state == "approved" and user_role != "editor":
            raise PermissionError("Only editors can approve articles")
        
        article.status = new_state
        article.updated_at = datetime.now()

# Main Service Layer
class ArticleService:
    """Responsibility: Orchestrating article operations"""
    def __init__(self):
        self.article_repo = ArticleRepository()
        self.author_repo = AuthorRepository()
        self.content_processor = ContentProcessor()
        self.validator = ContentValidator()
        self.search_engine = SearchEngine()
        self.analytics = AnalyticsTracker()
        self.cache = CacheManager()
        self.notifications = NotificationService()
        self.workflow = PublishingWorkflow()
    
    def create_article(self, title, content, author_id, category_id):
        # Create new article
        article = Article(title, content, author_id, category_id)
        
        # Validate content
        errors = self.validator.validate_article(article)
        if errors:
            raise ValueError(f"Validation failed: {', '.join(errors)}")
        
        # Process content
        article.content = self.content_processor.process_markdown(content)
        
        # Save article
        self.article_repo.save(article)
        
        return article
    
    def publish_article(self, article_id, user_role):
        article = self.article_repo.find_by_id(article_id)
        
        # Use workflow to manage state transitions
        self.workflow.transition_article(article, "published", user_role)
        
        # Save updated article
        self.article_repo.save(article)
        
        # Cache published article
        self.cache.cache_article(article)
        
        # Send notifications
        self.notifications.notify_article_published(article)
        
        return article
    
    def get_article(self, article_id, user_id=None):
        # Try cache first
        article = self.cache.get_cached_article(article_id)
        
        if not article:
            article = self.article_repo.find_by_id(article_id)
            if article and article.status == "published":
                self.cache.cache_article(article)
        
        # Track view
        if article and user_id:
            self.analytics.track_article_view(article_id, user_id)
        
        return article

Advanced Benefits:

  • Microservice-ready: Each class could become a separate service
  • Testable architecture: Mock any component for testing
  • Scalable design: Cache, search, and analytics can scale independently
  • Maintainable workflow: Publishing logic is isolated and configurable
  • Performance optimized: Caching and analytics don't affect core logic

Real-World Applications

Industry Examples

1. Banking System

# ❌ Violation: Account class doing everything
class BankAccount:
    def deposit(self, amount): pass
    def withdraw(self, amount): pass
    def send_sms_notification(self): pass
    def calculate_interest(self): pass
    def generate_statement(self): pass
    def validate_transaction(self): pass

# ✅ SRP Applied: Separate responsibilities
class Account:
    """Data representation"""
    pass

class TransactionProcessor:
    """Transaction operations"""
    pass

class NotificationService:
    """Customer communications"""
    pass

class InterestCalculator:
    """Interest calculations"""
    pass

class StatementGenerator:
    """Document generation"""
    pass

class TransactionValidator:
    """Business rule validation"""
    pass

2. E-learning Platform

# Separate responsibilities for different stakeholders
class Course:
    """Course data - Content team"""
    pass

class EnrollmentManager:
    """Enrollment logic - Academic team"""
    pass

class ProgressTracker:
    """Learning analytics - Data team"""
    pass

class CertificateGenerator:
    """Certification - Compliance team"""
    pass

class PaymentProcessor:
    """Billing - Finance team"""
    pass

3. Healthcare System

class Patient:
    """Patient data - Medical records team"""
    pass

class AppointmentScheduler:
    """Scheduling - Operations team"""
    pass

class BillingCalculator:
    """Insurance/billing - Finance team"""
    pass

class MedicalReportGenerator:
    """Reports - Clinical team"""
    pass

class NotificationService:
    """Communications - IT team"""
    pass

Framework Integration

Django/Flask Web Applications

# Models (Data representation)
class User(models.Model):
    username = models.CharField(max_length=100)
    email = models.EmailField()

# Services (Business logic)
class UserRegistrationService:
    def __init__(self):
        self.validator = UserValidator()
        self.email_service = EmailService()
        self.repository = UserRepository()

# Views (HTTP handling)
class UserRegistrationView:
    def post(self, request):
        service = UserRegistrationService()
        return service.register_user(request.data)

# Serializers (Data transformation)
class UserSerializer:
    def serialize(self, user):
        return {"id": user.id, "username": user.username}

API Design with SRP

# Separate endpoints for different responsibilities
class UserController:
    """User CRUD operations"""
    def get_user(self, user_id): pass
    def update_user(self, user_id, data): pass

class AuthController:
    """Authentication operations"""
    def login(self, credentials): pass
    def logout(self, token): pass

class ProfileController:
    """Profile management"""
    def update_profile(self, user_id, profile_data): pass
    def upload_avatar(self, user_id, image): pass

Best Practices

1. Class Design Guidelines

Naming Conventions

# ✅ Clear, single-purpose names
class UserValidator:        # Validates users
class EmailSender:         # Sends emails
class PaymentProcessor:    # Processes payments
class ReportGenerator:     # Generates reports

# ❌ Vague or multi-purpose names
class UserManager:         # What does it manage?
class DataHandler:         # What kind of data?
class SystemUtility:       # Too generic

Method Organization

class OrderValidator:
    """All methods should support the validation responsibility"""
    
    def validate_order(self, order):
        """Main validation method"""
        pass
    
    def validate_items(self, items):
        """Helper validation method"""
        pass
    
    def validate_customer(self, customer):
        """Helper validation method"""
        pass
    
    # ❌ Don't add unrelated methods
    # def send_email(self, order):  # This belongs in EmailService
    # def calculate_total(self, order):  # This belongs in PricingCalculator

2. Dependency Management

Constructor Injection

class OrderService:
    def __init__(self, validator, repository, email_service):
        self.validator = validator
        self.repository = repository
        self.email_service = email_service
    
    def process_order(self, order):
        if self.validator.validate_order(order):
            self.repository.save(order)
            self.email_service.send_confirmation(order)

Factory Pattern for Complex Dependencies

class ServiceFactory:
    @staticmethod
    def create_order_service():
        validator = OrderValidator()
        repository = OrderRepository()
        email_service = EmailService()
        return OrderService(validator, repository, email_service)

3. Testing Strategies

Unit Testing Single Responsibilities

import unittest
from unittest.mock import Mock

class TestOrderValidator(unittest.TestCase):
    def setUp(self):
        self.validator = OrderValidator()
    
    def test_valid_order_passes_validation(self):
        order = Order(customer_id=1, items=[{"name": "item", "quantity": 1}])
        result = self.validator.validate_order(order)
        self.assertTrue(result)
    
    def test_empty_order_fails_validation(self):
        order = Order(customer_id=1, items=[])
        with self.assertRaises(ValueError):
            self.validator.validate_order(order)

class TestOrderService(unittest.TestCase):
    def setUp(self):
        self.mock_validator = Mock()
        self.mock_repository = Mock()
        self.mock_email_service = Mock()
        self.service = OrderService(
            self.mock_validator,
            self.mock_repository,
            self.mock_email_service
        )
    
    def test_process_order_with_valid_order(self):
        order = Order(customer_id=1, items=[{"name": "item", "quantity": 1}])
        self.mock_validator.validate_order.return_value = True
        
        result = self.service.process_order(order)
        
        self.mock_validator.validate_order.assert_called_once_with(order)
        self.mock_repository.save.assert_called_once_with(order)
        self.mock_email_service.send_confirmation.assert_called_once_with(order)

4. Configuration and Environment

Separate Configuration Responsibility

class DatabaseConfig:
    """Responsibility: Database configuration"""
    def __init__(self):
        self.host = os.getenv('DB_HOST', 'localhost')
        self.port = os.getenv('DB_PORT', 5432)
        self.database = os.getenv('DB_NAME', 'myapp')

class EmailConfig:
    """Responsibility: Email configuration"""
    def __init__(self):
        self.smtp_host = os.getenv('SMTP_HOST')
        self.smtp_port = os.getenv('SMTP_PORT', 587)
        self.username = os.getenv('SMTP_USERNAME')

class AppConfig:
    """Responsibility: Application configuration orchestration"""
    def __init__(self):
        self.database = DatabaseConfig()
        self.email = EmailConfig()
        self.debug = os.getenv('DEBUG', 'False').lower() == 'true'

5. Error Handling

Specific Exception Classes

class ValidationError(Exception):
    """Raised by validators"""
    pass

class PersistenceError(Exception):
    """Raised by repositories"""
    pass

class CommunicationError(Exception):
    """Raised by notification services"""
    pass

# Each service handles its own error types
class OrderValidator:
    def validate_order(self, order):
        if not order.items:
            raise ValidationError("Order must contain items")

class OrderRepository:
    def save(self, order):
        try:
            # Database save logic
            pass
        except DatabaseError as e:
            raise PersistenceError(f"Failed to save order: {e}")

Common Pitfalls

1. Over-Engineering

Too Many Small Classes

# Don't create a class for every single method
class StringUppercaser:
    def uppercase(self, text):
        return text.upper()

class StringLowercaser:
    def lowercase(self, text):
        return text.lower()

# ✅ Group related functionality appropriately
class StringFormatter:
    def uppercase(self, text):
        return text.upper()
    
    def lowercase(self, text):
        return text.lower()
    
    def title_case(self, text):
        return text.title()

Premature Abstraction

# Don't create interfaces for everything immediately
class IUserValidator:
    def validate(self, user): pass

class IUserRepository:
    def save(self, user): pass

# ✅ Start simple, add abstractions when needed
class UserValidator:
    def validate(self, user): pass

class UserRepository:
    def save(self, user): pass

2. Misunderstanding Responsibility

Confusing Technical vs Business Responsibilities

# Technical responsibility (how) vs Business responsibility (what)
class UserService:
    def save_user_to_mysql(self, user):  # ❌ Technical detail leaked
        pass
    
    def save_user_via_http_api(self, user):  # ❌ Technical detail leaked
        pass

# ✅ Focus on business responsibility
class UserService:
    def save_user(self, user):  # ✅ Business operation
        pass

class MySQLUserRepository:  # ✅ Technical implementation separate
    def save(self, user):
        pass

3. Ignoring Context

Consider Your Application Size

# For small applications, this might be overkill:
class SimpleCalculator:
    def add(self, a, b):
        return a + b

class AdditionValidator:
    def validate_numbers(self, a, b):
        return isinstance(a, (int, float)) and isinstance(b, (int, float))

class AdditionLogger:
    def log_operation(self, a, b, result):
        print(f"{a} + {b} = {result}")

# For small apps, this is often sufficient:
class Calculator:
    def add(self, a, b):
        if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
            raise ValueError("Arguments must be numbers")
        result = a + b
        print(f"{a} + {b} = {result}")
        return result

4. Circular Dependencies

Classes Depending on Each Other

class UserService:
    def __init__(self):
        self.order_service = OrderService()  # ❌ Circular dependency
    
    def get_user_orders(self, user_id):
        return self.order_service.get_orders_by_user(user_id)

class OrderService:
    def __init__(self):
        self.user_service = UserService()  # ❌ Circular dependency
    
    def get_order_user(self, order_id):
        order = self.get_order(order_id)
        return self.user_service.get_user(order.user_id)

Proper Dependency Direction

class UserRepository:
    def find_by_id(self, user_id): pass

class OrderRepository:
    def find_by_user_id(self, user_id): pass
    def find_by_id(self, order_id): pass

class UserService:
    def __init__(self, user_repo, order_repo):
        self.user_repo = user_repo
        self.order_repo = order_repo
    
    def get_user_with_orders(self, user_id):
        user = self.user_repo.find_by_id(user_id)
        orders = self.order_repo.find_by_user_id(user_id)
        return {"user": user, "orders": orders}

Exercises

🎯 Practice Problems

Now it's time to apply what you've learned! Work through these exercises to master the Single Responsibility Principle.

Exercise 1: Basic Level

File: 01-basic-single-responsibility.py

Problem: You have a User class that handles user information, authentication, and email notifications. Refactor it to follow SRP.

Your Task:

  1. Identify the different responsibilities in the current code
  2. Create separate classes for each responsibility
  3. Ensure each class has a single, clear purpose
  4. Test your refactored code

Learning Goals:

  • Recognize SRP violations
  • Practice extracting classes
  • Understand single responsibility benefits

Exercise 2: Advanced Level

File: 02-advanced-single-responsibility.py

Problem: You have an Order class that handles order processing, payment, inventory, and notifications. This is a complex real-world scenario.

Your Task:

  1. Analyze the multiple responsibilities
  2. Design a proper class structure
  3. Implement proper dependency injection
  4. Create a service layer to orchestrate operations

Learning Goals:

  • Handle complex business logic
  • Design service architectures
  • Manage dependencies properly
  • Apply SRP in real-world scenarios

💡 Self-Assessment Questions

Before looking at the solutions, ask yourself:

  1. Identification: Can you list all the responsibilities in the original class?
  2. Separation: What would be the name and purpose of each new class?
  3. Dependencies: How would these classes interact with each other?
  4. Testing: How would you test each class independently?
  5. Benefits: What specific benefits does your refactoring provide?

🔍 Code Review Checklist

When reviewing your solution:

  • Single Purpose: Each class has one clear responsibility
  • Clear Naming: Class names clearly indicate their purpose
  • Cohesive Methods: All methods in a class support its responsibility
  • Loose Coupling: Classes don't depend on implementation details
  • Easy Testing: Each class can be tested independently
  • Future Changes: Changes to one responsibility don't affect others

🚀 Challenge Exercises

Ready for more? Try these additional challenges:

Challenge 1: Library Management System

Create a library system with these requirements:

  • Book management (add, remove, search)
  • Member management (register, update, deactivate)
  • Borrowing system (checkout, return, renewals)
  • Fine calculation (overdue fees, damage fees)
  • Notification system (due date reminders, overdue notices)
  • Report generation (popular books, member activity)

Focus: Design this system following SRP from the start.

Challenge 2: Social Media Platform

Design a social media platform with:

  • User profiles and authentication
  • Post creation and management
  • Friend/follower relationships
  • News feed generation
  • Notification system
  • Content moderation
  • Analytics and metrics

Focus: Handle complex interactions while maintaining SRP.

Challenge 3: E-commerce Refactoring

Take an existing e-commerce codebase (or create a monolithic one) and refactor it to follow SRP. Include:

  • Product catalog
  • Shopping cart
  • Order processing
  • Payment handling
  • Inventory management
  • Customer service
  • Analytics

Focus: Practice refactoring existing code to follow SRP.


Summary

🎯 Key Takeaways

What is SRP?

  • Definition: A class should have only one reason to change
  • Purpose: Each class should have a single, well-defined responsibility
  • Goal: Create focused, maintainable, and testable code

How to Apply SRP

  1. Identify Responsibilities: Ask "What does this class do?" and "Why would it change?"
  2. Separate Concerns: Extract different responsibilities into separate classes
  3. Use Composition: Combine single-responsibility classes to create complex behavior
  4. Name Clearly: Use descriptive names that reflect the single responsibility

Benefits of SRP

  • Easier Maintenance: Changes are localized to specific responsibilities
  • Better Testing: Each responsibility can be tested independently
  • Improved Readability: Code purpose is clear and predictable
  • Reduced Coupling: Classes are less dependent on each other
  • Enhanced Reusability: Focused classes can be reused in different contexts

Common Patterns

  • Data Models: Represent data structure only
  • Repositories: Handle data persistence
  • Services: Implement business logic
  • Validators: Enforce business rules
  • Processors: Transform or manipulate data
  • Notifiers: Handle communications

🔍 Recognition Patterns

SRP Violations (Red Flags)

  • Classes with "AND" in their description
  • Large classes (>200-300 lines)
  • Many methods (>10-15 methods)
  • Multiple import statements from different domains
  • Frequent changes for different reasons
  • Mixed abstraction levels

Good SRP Implementation (Green Flags)

  • Clear, single-purpose class names
  • Cohesive method groups
  • Single reason to change
  • Easy to test independently
  • Clear dependencies
  • Focused documentation

📈 Progression Path

Beginner Level

  • Recognize obvious SRP violations
  • Extract simple responsibilities (validation, formatting)
  • Practice with basic examples
  • Focus on clear naming

Intermediate Level

  • Handle complex business scenarios
  • Design service layers
  • Manage dependencies properly
  • Apply SRP in web applications

Advanced Level

  • Design microservice-ready architectures
  • Handle cross-cutting concerns
  • Balance SRP with performance
  • Mentor others in SRP application

🛠️ Practical Application

In Your Daily Work

  1. Code Reviews: Look for SRP violations in pull requests
  2. New Features: Design new classes following SRP from the start
  3. Refactoring: Gradually extract responsibilities from large classes
  4. Architecture: Use SRP to guide system design decisions

Team Practices

  • Coding Standards: Include SRP guidelines in your team's standards
  • Design Reviews: Discuss responsibilities during design sessions
  • Training: Share SRP knowledge with team members
  • Metrics: Track class sizes and complexity as SRP indicators

🎓 Next Steps

Continue Learning

  1. Practice: Work through the exercises multiple times
  2. Apply: Use SRP in your current projects
  3. Study: Look at well-designed open-source projects
  4. Teach: Explain SRP to others to deepen your understanding

Related Topics

  • Open/Closed Principle: Build on SRP for extensible design
  • Dependency Injection: Manage SRP class dependencies
  • Design Patterns: Apply patterns that support SRP
  • Clean Architecture: Use SRP as a foundation for clean architecture

💭 Final Thoughts

The Single Responsibility Principle is more than just a coding guideline—it's a way of thinking about software design. When you consistently apply SRP:

  • Your code becomes more professional and easier to work with
  • Your team becomes more productive with clearer responsibilities
  • Your applications become more maintainable and adaptable to change
  • Your skills as a developer improve through disciplined design thinking

Remember: Perfect is the enemy of good. Start applying SRP gradually, learn from experience, and continuously improve your design skills.

🔗 Resources for Continued Learning

Books

  • "Clean Code" by Robert C. Martin
  • "Clean Architecture" by Robert C. Martin
  • "Refactoring" by Martin Fowler

Online Resources

Practice Platforms

  • Work through the provided exercises
  • Contribute to open-source projects
  • Code review sessions with peers
  • Design pattern practice problems

🎉 Congratulations! You've completed the comprehensive Single Responsibility Principle tutorial. You're now equipped with the knowledge and skills to write more maintainable, testable, and professional code.

Ready for the next challenge? Continue with the Open/Closed Principle to further enhance your SOLID design skills!


Happy coding! 🚀