SOLID is five famous principles that keep object-oriented code easy to read and change. Each letter is one rule. You don't need them on day one — but knowing them makes you a stronger designer.
Letter
Principle
Plain English
S
Single Responsibility
One class, one job.
O
Open/Closed
Add features without editing old code.
L
Liskov Substitution
A subclass should work anywhere its parent does.
I
Interface Segregation
Many small interfaces beat one giant one.
D
Dependency Inversion
Depend on ideas, not exact details.
1️⃣ S — Single Responsibility
A class should have one reason to change. Don't mix saving files with calculating math.
# 😵 Does too much
class Report:
def calculate(self): ...
def save_to_file(self): ... # different job!
# 😀 Split responsibilities
class Report:
def calculate(self): ...
class ReportSaver:
def save(self, report): ...
2️⃣ O — Open/Closed
Code should be open to extension, closed to modification. Add new shapes without rewriting the old ones.
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r * self.r
class Square(Shape):
def __init__(self, s): self.s = s
def area(self): return self.s * self.s
💡 The win
To add a Triangle, you write a new class — you never touch Circle or Square. Less risk of breaking working code!
3️⃣ L, I, D in one breath
Liskov: if Penguin is a Bird, it shouldn't break code that calls bird.fly() — so maybe not every bird should have fly().
Interface Segregation: don't force a class to implement methods it doesn't use.
Dependency Inversion: a NotificationService should depend on a general "sender," not specifically on "EmailSender" — so you can swap in SMS later.
✅ What you learned
Single Responsibility — one class, one job.
Open/Closed — extend without editing.
Liskov — subclasses must behave like their parent.
Interface Segregation — keep interfaces small.
Dependency Inversion — depend on abstractions.
🎮 Time to Practice!
Refactor toward SOLID design. 🧱
1️⃣
Task 1: Split Responsibilities
Medium
Keep Report only for calculating (a total() method) and put saving in a separate ReportSaver with a save(value) method. Use the numbers [20, 22] and print the total, then the saved message.
Write a Shape base class and a Circle (use 3.14 for π), then add a Square that extends Shapewithout editing Circle at all. Print the area of Circle(5) and Square(4).
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14 * self.r * self.r
class Square(Shape):
def __init__(self, s):
self.s = s
def area(self):
return self.s * self.s
print(Circle(5).area())
print(Square(4).area())
🔌
Task 3: Dependency Inversion
Boss
Write EmailSender and SmsSender, both with a send(msg) method, and a Notifier that accepts any sender in __init__. Swapping email for SMS must need no change inside Notifier. Send "Hi!" both ways.
class EmailSender:
def send(self, msg):
return "Sending via Email: " + msg
class SmsSender:
def send(self, msg):
return "Sending via SMS: " + msg
class Notifier:
def __init__(self, sender):
self.sender = sender
def notify(self, msg):
return self.sender.send(msg)
print(Notifier(EmailSender()).notify("Hi!"))
print(Notifier(SmsSender()).notify("Hi!"))
🏋️ Extra Practice — Engineering Reps
Five letters, five habits that keep big code from rotting. Practice each one on a small example so you recognize it in a big one.
🦆
Task 4: L — Liskov: Subclasses Must Behave
Tricky
A Bird has move(). If Penguin inherits fly() and then crashes, the substitution rule is broken. Fix it: give Bird a move(), let Sparrow return "flying" and Penguin return "swimming", then loop over both calling move() — no crashes, no special cases.
Sparrow is flying
Penguin is swimming
class Bird:
def move(self):
return "moving"
class Sparrow(Bird):
def move(self):
return "flying"
class Penguin(Bird):
def move(self):
return "swimming"
for bird in [Sparrow(), Penguin()]:
print(f"{type(bird).__name__} is {bird.move()}")
🔌
Task 5: I — Interface Segregation
Tricky
Do not force a class to have methods it cannot use. Instead of one fat Machine with print, scan and fax, write small classes: Printer with print_page(), Scanner with scan(), and AllInOne(Printer, Scanner) that gets both. Print the result of all three actions.
printing
printing
scanning
class Printer:
def print_page(self):
return "printing"
class Scanner:
def scan(self):
return "scanning"
class AllInOne(Printer, Scanner):
pass
simple = Printer()
machine = AllInOne()
print(simple.print_page())
print(machine.print_page())
print(machine.scan())
✂️
Task 6: S — One Class, One Reason to Change
Medium
A Student class should not also know how to save files and format reports. Split it: Student holds the data and average(); StudentReport takes a student and returns a printable line. Print the report for a student with grades 80, 90, 100.
A price_for(kind) function full of elifs must be edited for every new ticket type. Replace it with a dictionary of prices plus a register(kind, price) function, so adding a "student" ticket needs no change to the lookup code. Print the adult price, register the student price, then print it too.
A Notifier should not hard-code email. Write EmailSender and SmsSender that both have send(message), and a Notifier that receives any sender in its __init__. Send the same message both ways and print each result.
Email: Dinner is ready
SMS: Dinner is ready
class EmailSender:
def send(self, message):
return f"Email: {message}"
class SmsSender:
def send(self, message):
return f"SMS: {message}"
class Notifier:
def __init__(self, sender):
self.sender = sender
def alert(self, message):
return self.sender.send(message)
print(Notifier(EmailSender()).alert("Dinner is ready"))
print(Notifier(SmsSender()).alert("Dinner is ready"))
🏗️
Task 9: Boss Level: All Five at Once
Boss
Build a mini payment system: an abstract idea of a PaymentMethod with pay(amount), two implementations (Card, Cash), and a Checkout class that takes any payment method and only handles totals. Pay $25.50 both ways and print each confirmation. Then say to yourself which letter each part of your design satisfies.
Charged $25.50 to card
Took $25.50 in cash
class PaymentMethod:
def pay(self, amount):
raise NotImplementedError
class Card(PaymentMethod):
def pay(self, amount):
return f"Charged ${amount:.2f} to card"
class Cash(PaymentMethod):
def pay(self, amount):
return f"Took ${amount:.2f} in cash"
class Checkout:
def __init__(self, method):
self.method = method
def buy(self, prices):
return self.method.pay(sum(prices))
print(Checkout(Card()).buy([10.00, 15.50]))
print(Checkout(Cash()).buy([10.00, 15.50]))