Single Responsibility Principle (SRP) in Python: A Practical Guide
Why SOLID Principles Matter
SOLID principles are the foundation of maintainable, scalable software. Today, we’ll focus on the Single Responsibility Principle (SRP), which states:
"A class should have only one reason to change."
In other words, each component should do one thing and do it well.
Violating SRP: A Common Pitfall
Let’s examine a User class that handles multiple responsibilities:
# Versión que viola SRP (pero más completa)
class User:
def __init__(self, name: str, email: str):
if not self._validate_email(email):
raise ValueError("Email inválido")
self.name = name
self.email = email
self._db_connection = self._connect_to_db()
self._email_service = self._init_email_service()
def _validate_email(self, email: str) -> bool:
"""Valida formato básico de email"""
return "@" in email and "." in email.split("@")[1]
def _connect_to_db(self):
"""Simula conexión a PostgreSQL"""
print("🔌 Conectando a PostgreSQL...")
return {"status": "connected", "db": "users_db"}
def _init_email_service(self):
"""Simula configuración de servicio de email"""
print("✉️ Inicializando SMTP...")
return {"smtp_server": "smtp.example.com", "port": 587}
def save_to_database(self):
"""Guarda usuario en DB con validación"""
if not hasattr(self, "name") or not hasattr(self, "email"):
raise ValueError("Datos de usuario incompletos")
query = f"""
INSERT INTO users (name, email)
VALUES ('{self.name}', '{self.email}')
"""
print(f"💾 Ejecutando query: {query}")
# Simulación de inserción
return {"status": "success", "user_id": 123}
def send_welcome_email(self):
"""Envía email con plantilla HTML"""
email_body = f"""
<html>
<body>
<h1>Bienvenido, {self.name}!</h1>
<p>Gracias por registrarte con el email {self.email}</p>
</body>
</html>
"""
print(f"📨 Enviando email a {self.email}:\n{email_body}")
return {"status": "sent", "email": self.email}
# Uso
try:
user = User("Ana López", "ana@example.com")
user.save_to_database()
user.send_welcome_email()
except ValueError as e:
print(f"❌ Error: {e}")
Key Problems Identified
Multiple Responsibilities
Handles validation, database operations, email delivery, and service configuration
Violates SRP by managing 4 distinct concerns
High Coupling
Tightly bound to PostgreSQL and SMTP implementations
Changing email providers requires modifying the
Userclass
Testing Complexity
Requires mocking database and email services simultaneously
Unit tests become fragile and interdependent
Maintenance Risks
Any change in email templating affects the core
UserclassDatabase schema changes force class modifications
Refactoring for SRP Compliance
We’ll split responsibilities into dedicated classes:
# ------ User Model (solo datos) ------
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
# ------ Validación separada ------
class UserValidator:
@staticmethod
def validate(user: User) -> bool:
return UserValidator._validate_email(user.email)
@staticmethod
def _validate_email(email: str) -> bool:
return "@" in email and "." in email.split("@")[1]
# ------ Database Service ------
class PostgreSQLUserRepository:
def __init__(self):
self._connection = self._connect()
def _connect(self):
print("🔌 Conectando a PostgreSQL...")
return {"status": "connected"}
def save(self, user: User):
query = f"INSERT INTO users (name, email) VALUES ('{user.name}', '{user.email}')"
print(f"💾 Ejecutando: {query}")
return {"status": "success"}
# ------ Email Service ------
class SendGridEmailService:
def __init__(self):
self._config = self._load_config()
def _load_config(self):
print("✉️ Configurando SendGrid...")
return {"api_key": "SG.123"}
def send_welcome(self, user: User):
email_body = self._build_template(user)
print(f"📨 Enviando a {user.email}:\n{email_body}")
return {"status": "sent"}
def _build_template(self, user: User):
return f"""
<html>
<body>
<h1>Bienvenido, {user.name}!</h1>
<p>Tu registro fue exitoso.</p>
</body>
</html>
"""
# ------ Orchestrator (coordina las dependencias) ------
class UserRegistration:
def __init__(self):
self.validator = UserValidator()
self.repository = PostgreSQLUserRepository()
self.email_service = SendGridEmailService()
def register(self, name: str, email: str):
user = User(name, email)
if not self.validator.validate(user):
raise ValueError("Email inválido")
self.repository.save(user)
self.email_service.send_welcome(user)
return user
# Uso
try:
registration = UserRegistration()
registration.register("Carlos Ruiz", "carlos@example.com")
except ValueError as e:
print(f"❌ Error: {e}")
Key Benefits
✅ Easier maintenance: Modify database logic without touching User.
✅ Better testing: Mock dependencies independently.
✅ Higher reusability: EmailService can be used across the app.
Real-World Analogy
Think of SRP like a restaurant:
Chefs cook food.
Waiters serve customers.
Cashiers handle payments.
Merging all roles into one person would be chaotic—just like violating SRP!
Conclusion
SRP reduces complexity by enforcing separation of concerns. Apply it to:
Avoid "god objects"
Simplify debugging
Enable team scalability
Challenge: Review your codebase—can you spot SRP violations?