DEV Community

Timevolt
Timevolt

Posted on

SRP and the Hero's Journey: Writing Code Like a Jedi Master

The Quest Begins (The "Why")

I still remember the first time I opened a legacy codebase and felt like I’d walked into the Death Star’s trash compactor. A single User class was juggling validation, hashing passwords, sending welcome emails, persisting to the database, and even generating PDF reports. Changing one tiny rule—like “users must now confirm their email before they can post”—required me to hunt through a 300‑line method, risk breaking something unrelated, and pray that my tests didn’t explode.

That experience taught me a painful lesson: when a class wears too many hats, every change feels like defusing a bomb while blindfolded. The code became fragile, tests grew sluggish, and onboarding new teammates felt like handing them a map written in ancient runes. I knew there had to be a better way, and that’s when the SOLID compass pointed me toward the Single Responsibility Principle (SRP).

The Revelation (The Insight)

SRP is beautifully simple: a class should have only one reason to change. If you can think of more than one motivation for editing a class, it’s probably doing too much. Think of it like a lightsaber—its sole purpose is to cut. If you start using it to toast bread, stir soup, and signal allies, you’ll quickly find yourself with a melted hilt and a very confused Jedi.

When each class owns a single responsibility, you gain:

  • Clarity – the class name tells you exactly what it does.
  • Testability – you can isolate behavior with minimal mocks.
  • Flexibility – swapping out an implementation (say, a different email provider) doesn’t ripple through unrelated code.
  • Safety – a change in one area is unlikely to break another.

That’s the magic: by narrowing focus, you actually broaden the system’s resilience.

Wielding the Power (Code & Examples)

The Trap: A “God” Class

Here’s the kind of code I used to write—everything bundled together because it felt convenient at the moment.

class User:
    def __init__(self, username, email, plain_password):
        self.username = username
        self.email = email
        self.hashed_password = self._hash_password(plain_password)

    def _hash_password(self, plain_password):
        # simplistic hashing for illustration
        import hashlib
        return hashlib.sha256(plain_password.encode()).hexdigest()

    def validate(self):
        if not self.username or len(self.username) < 3:
            raise ValueError("Username too short")
        if "@" not in self.email:
            raise ValueError("Invalid email")
        # more validation logic...

    def save(self):
        # pretend this talks to a DB
        print(f"Saving user {self.username} to the database")

    def send_welcome_email(self):
        # pretend this talks to an email service
        print(f"Sending welcome email to {self.email}")

    def generate_report(self):
        # pretend this creates a PDF
        print(f"Generating activity report for {self.username}")
Enter fullscreen mode Exit fullscreen mode

What went wrong?

  • Multiple reasons to change: validation rules, hashing algorithm, persistence mechanism, email template, report format—all live here.
  • Testing nightmare: to test validate*validate`, I still had to instantiate the whole class, which dragged in dependencies I didn’t care about.
  • Coupling: if I wanted to swap the email provider, I’d have to touch this class, risking a bug in validation or storage.

The Victory: Splitting Responsibilities

Applying SRP means extracting each concern into its own collaborator. The User becomes a simple data holder, while dedicated services handle the heavy lifting.

`python

1. Pure data model – only knows about its attributes

class User:
def init(self, username, email, hashed_password):
self.username = username
self.email = email
self.hashed_password = hashed_password

2. Validation – one reason to change: validation rules

class UserValidator:
def validate(self, user: User):
if not user.username or len(user.username) < 3:
raise ValueError("Username too short")
if "@" not in user.email:
raise ValueError("Invalid email")
# additional rules can be added here without touching other classes

3. Password hashing – one reason to change: hashing algorithm

class PasswordHasher:
def hash(self, plain_password: str) -> str:
import hashlib
return hashlib.sha256(plain_password.encode()).hexdigest()

4. Persistence – one reason to change: storage mechanism

class UserRepository:
def save(self, user: User):
# DB logic goes here
print(f"Saving user {user.username} to the database")

5. Notification – one reason to change: email service

class EmailService:
def send_welcome(self, user: User):
# email logic goes here
print(f"Sending welcome email to {user.email}")

6. Reporting – one reason to change: report format

class ReportGenerator:
def generate(self, user: User):
# report logic goes here
print(f"Generating activity report for {user.username}")
`

Using the new pieces:

`python
hasher = PasswordHasher()
hashed = hasher.hash("super_secret")
new_user = User("alice", "alice@example.com", hashed)

validator = UserValidator()
validator.validate(new_user) # raises if something’s off

repo = UserRepository()
repo.save(new_user)

mailer = EmailService()
mailer.send_welcome(new_user)

reporter = ReportGenerator()
reporter.generate(new_user)
`

Why this feels better:

  • Each class is tiny and focused.
  • I can unit‑test UserValidator without pulling in a database or email client.
  • If the company switches from SHA‑256 to Argon2, I only edit PasswordHasher.
  • Adding a new notification channel (SMS, push) means creating a new NotificationService—no existing code needs to be touched.

The before‑after contrast is stark: the original User was a tangle of concerns; the refactored version is a collection of polite, single‑purpose coworkers that communicate through clear interfaces.

Why This New Power Matters

When you start obeying SRP, you stop fighting the code and start building with it.

  • Speed of delivery – a developer can add a feature in one isolated class without fear of breaking something three layers away.
  • Bug reduction – defects are easier to pinpoint because the responsible class is small and well‑defined.
  • Team scalability – newcomers can own a single responsibility (e.g., “I’ll handle the email service”) and become productive faster.
  • Future‑proofing – swapping out a database, a payment gateway, or a third‑party API becomes a matter of plugging in a new implementation, not rewiring half the system.

In short, SRP turns a monolithic beast into a legion of specialized soldiers, each knowing exactly where to stand and what to do.

Your Turn: Embark on Your Own Quest

Look at the code you’re working on right now. Find a class that does more than one thing—maybe it validates input, talks to a service, and formats a response all at once. Spend ten minutes extracting one of those responsibilities into its own collaborator. Write a test for the new piece, watch your confidence grow, and feel the shift from “I’m scared to touch this” to “I can change this safely.”

What’s the first class you’ll refactor? Drop a comment below with your before/after snippet—let’s celebrate each other’s victories! 🚀

Top comments (0)