Phase 5 · Low-level design: OOP and SOLID · Topic 5.3

SOLID: S — Single Responsibility Principle

Must know

A class should have one reason to change: one job.

Not started

Step 1 Understand it

The problem it solves. When one does several jobs, unrelated changes collide: a change to the email wording risks breaking the tax calculation, and every change means retesting everything.

Watch it happen

AnimationSingle responsibility: one class, one reason to change
Read the animation as text
  1. One Invoice class calculates totals, saves itself to the database and emails itself. Three different people ask for changes to it.
  2. Marketing asks for a new email layout. Editing send_email() in the same class risks breaking the totals — finance's code — by accident.
  3. Split it: one class per reason to change. Invoice calculates, InvoiceRepository saves, InvoiceEmailer sends.
  4. A small coordinating function calls them in order. Now an email change touches only InvoiceEmailer.

Picture it

classDiagram
  class InvoiceService {
    +process(invoice)
  }
  class Invoice {
    +total()
  }
  class InvoiceRepository {
    +save(invoice)
  }
  class InvoiceEmailer {
    +send(invoice)
  }
  InvoiceService --> Invoice
  InvoiceService --> InvoiceRepository
  InvoiceService --> InvoiceEmailer
After splitting: each class has one job, and a small service connects them.

The idea

If one class holds the business rules, the saving and the email sending, a change to any one of them risks breaking the others. Split it so each class does one thing well and changes for one reason.

How it works, step by step

  1. List what the class does, and who asks for changes to each part (finance, the database team, marketing).
  2. Each different 'who' is a separate responsibility.
  3. Move each responsibility into its own class with a clear name.
  4. Connect them in a small coordinating function or service.

Analogy

A restaurant where the chef cooks, the cashier takes payments and the cleaner cleans. If the chef also did the accounts, every new tax rule would interrupt the cooking.

Where the analogy breaks

In a tiny café one person does everything, and that's fine. Splitting too early into very small classes adds complexity; apply this when responsibilities really do change separately.

Example

Invoice calculates the totals, InvoiceRepository saves it, InvoiceEmailer sends it.

Must-know points

  • 'A reason to change' usually means a different person or team asking for changes.
  • Warning signs: huge classes, many unrelated imports, that never use each other's data.
  • The result: smaller classes that are easier to test.

Common mistakes

  • Splitting by the number of methods instead of by reason to change.
  • Creating dozens of tiny classes that are always changed together.
python
# Before: one class, three reasons to change
class Invoice:
    def total(self): ...
    def save_to_db(self): ...
    def email_to_customer(self): ...

# After: one job each
class Invoice:
    def total(self): ...

class InvoiceRepository:
    def save(self, invoice): ...

class InvoiceEmailer:
    def send(self, invoice): ...

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.

Step 3 Practise

Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.

  1. ExplainSay it in plain words in under a minute, with no notes.

    Explain SRP with the restaurant picture.

    Show answer

    In a restaurant, the chef cooks, the cashier takes payments and the cleaner cleans. When a new tax rule arrives, only the cashier changes how they work; the kitchen carries on undisturbed.

    If the chef also did the accounts, every tax change would interrupt the cooking, and a mistake in the accounts could ruin dinner service. That is a class with several responsibilities: a change for one reason (tax) risks breaking something unrelated (cooking).

    The single responsibility principle says each class should have one reason to change, meaning one job, owned by one kind of change. An Invoice works out totals (changes when pricing rules change); a repository saves it (changes when the database changes); an emailer sends it (changes when the email changes).

  2. DrawSketch the diagram (or class diagram) from memory.

    Draw the before and after for the Invoice example.

    Show answer

    Before, one class changes for three unrelated reasons, and a change to the email template means redeploying, and risking, the tax logic. After, each class has one reason to change, and each can be tested on its own (the repository with a test database, the emailer with a fake sender).

    text
    Before: one class, three reasons to change
    ┌───────────────────────┐
    │ Invoice               │  pricing and tax rules
    │  + total()            │  the database
    │  + save_to_db()       │  the email template
    │  + email_to_customer()│
    └───────────────────────┘
    
    After: one reason each
    ┌────────────┐  ┌───────────────────┐  ┌────────────────┐
    │ Invoice    │  │ InvoiceRepository │  │ InvoiceEmailer │
    │  + total() │  │  + save(invoice)  │  │  + send(invoice)│
    └────────────┘  └───────────────────┘  └────────────────┘
     pricing          the database           the email
  3. ApplyUse it in a real design question and say exactly where it fits.

    A UserService validates input, passwords, saves users and sends welcome emails. Split it, and write the new class names and methods.

    Show answer

    Four jobs, four classes, each with one reason to change:
    • UserValidator.validate(email, password): the sign-up rules.
    • PasswordHasher.hash(password): the hashing algorithm.
    • UserRepository.save(user) and exists(email): the database.
    • WelcomeEmailer.send(user): the email's wording or provider.

    A thin UserRegistration.register(email, password) coordinates them in order and holds no rules of its own. Each part can be swapped or tested alone: here the repository and the emailer are in-memory stand-ins.

    python
    import hashlib
    import os
    import re
    
    
    class UserValidator:                        # changes when the sign-up rules change
        def validate(self, email, password):
            if not re.fullmatch(r"[^@\s]+@[^@\s]+\.[a-z]+", email):
                raise ValueError("invalid email")
            if len(password) < 10:
                raise ValueError("password too short")
    
    
    class PasswordHasher:                       # changes when the hashing algorithm changes
        def hash(self, password):
            salt = os.urandom(16)
            key = hashlib.scrypt(password.encode(), salt=salt, n=2**14, r=8, p=1)
            return salt.hex() + "$" + key.hex()
    
    
    class UserRepository:                       # changes when the database changes
        def __init__(self):
            self.users = {}
    
        def exists(self, email):
            return email in self.users
    
        def save(self, user):
            self.users[user["email"]] = user
    
    
    class WelcomeEmailer:                       # changes when the email changes
        def __init__(self):
            self.sent = []
    
        def send(self, user):
            self.sent.append(f"Welcome, {user['email']}!")
    
    
    class UserRegistration:                     # only coordinates the steps
        def __init__(self, validator, hasher, repository, emailer):
            self.validator, self.hasher = validator, hasher
            self.repository, self.emailer = repository, emailer
    
        def register(self, email, password):
            self.validator.validate(email, password)
            if self.repository.exists(email):
                raise ValueError("email already registered")
            user = {"email": email, "password_hash": self.hasher.hash(password)}
            self.repository.save(user)
            self.emailer.send(user)
            return user
    
    
    emailer = WelcomeEmailer()
    registration = UserRegistration(UserValidator(), PasswordHasher(), UserRepository(), emailer)
    registration.register("priya@mail.com", "correct horse battery")
    print(emailer.sent)                         # ['Welcome, priya@mail.com!']
  4. Trade-offsSay what it costs, when not to use it, and the alternative.

    When does splitting classes make code worse?

    Show answer

    • When the pieces always change together. If every change touches the same three classes, they were one responsibility, and splitting them only scatters it: you now open three files to follow one idea.
    • When the classes become too small to mean anything: lots of one-method classes that just pass the call along ("ravioli code"). Following one means jumping through ten files.
    • When you split early, on guesses. Before you know what actually changes, you will often split along the wrong lines, and then have to change several classes for every request.
    • When the wiring costs more than the class: every split adds construction, dependency passing and names to remember.

    The test is "different reasons to change", meaning different people or rules that change at different times, not "this class has more than 100 lines".

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.

Single responsibility means each class has one reason to change. For example, I'd separate invoice calculation, saving and emailing, so a change to the email template can't break the tax logic.

Follow-up questions

Isn't that just 'make classes small'?

No, it's about cohesion: things that change together stay together, and things that change for different reasons are separated.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.

  • Explain Say it in plain words in under a minute, with no notes.
  • Draw Sketch the diagram (or class diagram) from memory.
  • Apply Use it in a real design question and say exactly where it fits.
  • Trade-offs Say what it costs, when not to use it, and the alternative.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.