DEV Community

Timevolt
Timevolt

Posted on

before_solid.py

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)
Enter fullscreen mode Exit fullscreen mode

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 usercreation flow.  

If the email template changes, Im not only part that is the email template youll also have to touch this class, risking a regression in validation or DB logic.  

### The Victory: Splitting Responsibilities  

Now lets apply SRP. Well extract each concern into its own class, then compose them in a thin orchestrates them.

Enter fullscreen mode Exit fullscreen mode


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)
Enter fullscreen mode Exit fullscreen mode

---------- 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)
Enter fullscreen mode Exit fullscreen mode



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!
Enter fullscreen mode Exit fullscreen mode

Top comments (0)