import re
import smtplib
from email.message import EmailMessage
from typing import Optional
class UserService:
def init(self, db_connection):
self.db = db_connection
def validate_email(self, email: str) -> bool:
# Very simple regex for demo purposes
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
return re.match(pattern, email) is not None
def create_user(self, name: str, email: str, password: str) -> Optional[int]:
if not self.validate_email(email):
raise ValueError("Invalid email address")
# Hash password (simplified)
hashed_pw = hash(password)
user_id = self.db.insert_user(name, email, hashed_pw)
self._send_welcome_email(email, name)
return user_id
def _send_welcome_email(self, email: str, name: str) -> None:
msg = EmailMessage()
msg.set_content(f"Hi {name}, welcome to our platform!")
msg['Subject'] = "Welcome!"
msg['From'] = "noreply@example.com"
msg['To'] = email
with smtplib.SMTP('localhost') as server:
server.send_message(msg)
def update_last_login(self, user_id: int) -> None:
self.db.update_last_login(user_id)
Look at all the hats this class wears:
* **Validator** – checks email format.
* **Repository** – talks to the database via `self.db`.
* **Notifier** – sends emails via SMTP.
* **Service coordinator** – orchestrates the whole user‑creation flow.
If the email template changes, I’m not only part that is the email template you’ll also have to touch this class, risking a regression in validation or DB logic.
### The Victory: Splitting Responsibilities
Now let’s apply SRP. We’ll extract each concern into its own class, then compose them in a thin orchestrates them.
python
after_solid.py
import re
import smtplib
from email.message import EmailMessage
from typing import Optional, Protocol
---------- Single‑Responsibility Components ----------
class EmailValidator:
def is_valid(self, email: str) -> bool:
pattern = r'^[\w.-]+@[\w.-]+.\w+$'
return re.match(pattern, email) is not None
class UserRepository(Protocol):
def insert_user(self, name: str, email: str, hashed_pw: int) -> int: ...
def update_last_login(self, user_id: int) -> None: ...
class SmtpNotifier:
def init(self, host: str = 'localhost'):
self.host = host
def send_welcome(self, email: str, name: str) -> None:
msg = EmailMessage()
msg.set_content(f"Hi {name}, welcome to our platform!")
msg['Subject'] = "Welcome!"
msg['From'] = "noreply@example.com"
msg['To'] = email
with smtplib.SMTP(self.host) as server:
server.send_message(msg)
---------- Orchestrator (still SRP‑compliant) ----------
class UserService:
def init(
self,
validator: EmailValidator,
repository: UserRepository,
notifier: SmtpNotifier,
):
self.validator = validator
self.repository = repository
self.notifier = notifier
def create_user(self, name: str, email: str, password: str) -> Optional[int]:
if not self.validator.is_valid(email):
raise ValueError("Invalid email address")
hashed_pw = hash(password)
user_id = self.repository.insert_user(name, email, hashed_pw)
self.notifier.send_welcome(email, name)
return user_id
def update_last_login(self, user_id: int) -> None:
self.repository.update_last_login(user_id)
What changed?
* **EmailValidator** only knows how to validate an email.
* **SmtpNotifier** only knows how to send an email via SMTP.
* **UserRepository** (a protocol) abstracts the persistence layer—now we can swap in a mock, a PostgreSQL adapter, or a NoSQL client without touching anything else.
* **UserService** now has a single responsibility: *orchestrating* the user‑creation workflow. It delegates the actual work to the focused collaborators.
If I need to change the email template, I edit `SmtpNotifier.send_welcome`. If I want to switch to a different validation library, I replace `EmailValidator`. The orchestrator stays untouched.
### The Traps to Avoid
1. **God Class Creep** – It’s tempting to add a “quick helper” method inside `UserService`. Resist the urge; instead, ask: *Does this belong to a collaborator?*
2. **Leaky Abstractions** – If your repository starts returning domain objects that also know how to send emails, you’ve pulled responsibility back into the wrong place. Keep interfaces narrow.
3. **Over‑Engineering** – SRP isn’t about making a million one‑line classes. It’s about giving each class a clear, cohesive purpose. If a class truly does one thing, even if that thing is a few related steps, you’re good.
## Why This New Power Matters
Now that I’ve split responsibilities, my tests are lightning‑fast. I can instantiate a fake `EmailValidator` that always returns `True`, a mock `UserRepository` that records calls, and a dummy `SmtpNotifier` that does nothing—then assert that the orchestrator calls each collaborator exactly once. No spinning up real SMTP servers or databases in unit tests.
When a product manager asks, “Can we add a CAPTCHA step before account creation?” I add a `CaptchaValidator` plugin, inject it into `UserService`, and leave the rest of the code untouched. The system feels **extensible**, not fragile.
Best of all, I sleep better knowing that a change in one corner of the codebase won’t unleash a cascade of bugs elsewhere. It’s like finally getting the right weapon for the right enemy in an RPG—you feel prepared, confident, and ready for the next quest.
## Your Turn
Give it a try: pick a class in your current project that feels like a Swiss‑army knife. List out every reason it might need to change. Then, extract each reason into its own collaborator. You’ll be amazed at how much clearer the flow becomes.
Got a story about a class you refactored using SRP? Drop it in the comments—I’d love to hear how it transformed your code (and maybe your sanity). Happy coding!
Top comments (0)