OOP Concepts · Lesson 6 of 8 · Updated ·
SOLID Principles — Five Rules for Clean Object-Oriented Code
SOLID is a set of five design rules that keep object-oriented code easy to change and test. They're asked in interviews for experienced roles and increasingly for freshers too.
S — Single Responsibility Principle
A class should have one reason to change.
❌ One Invoice class that calculates totals, saves to the database and sends emails. A change to email wording forces you to touch billing code.
✅ Split it:
class Invoice:
def __init__(self, items): self.items = items
def total(self): return sum(price * qty for price, qty in self.items)
class InvoiceRepository:
def save(self, invoice): return f"saved invoice of {invoice.total()}"
class InvoiceMailer:
def send(self, invoice, to): return f"mailed {invoice.total()} to {to}"
inv = Invoice([(499, 2), (99, 1)])
print(inv.total(), "|", InvoiceRepository().save(inv), "|", InvoiceMailer().send(inv, "asha@x.in"))
1097 | saved invoice of 1097 | mailed 1097 to asha@x.in
O — Open/Closed Principle
Classes should be open for extension, closed for modification.
❌ Every new discount type means editing an if … elif … chain inside apply_discount().
✅ Add a new class instead:
class Discount:
def apply(self, price): return price
class Festive(Discount):
def apply(self, price): return price * 0.8
class Student(Discount):
def apply(self, price): return price - 200
def checkout(price, discount: Discount):
return discount.apply(price)
print(checkout(2000, Festive()), checkout(2000, Student()), checkout(2000, Discount()))
1600.0 1800 2000
A new Diwali discount is a new class — checkout never changes.
L — Liskov Substitution Principle
A child class must work anywhere its parent works, without surprises.
Classic violation: Square extends Rectangle. Code that sets width = 5 and height = 4 expects area 20, but a Square forces both sides equal and breaks it. If a subclass needs to refuse or change the meaning of a parent method, it shouldn't be a subclass.
Signs of violation: a subclass throws "not supported" for an inherited method, or callers need if isinstance(x, Child) checks.
I — Interface Segregation Principle
Many small, focused interfaces are better than one big one.
❌ An interface Machine with print(), scan(), fax() forces a simple printer to implement scan() and fax() with errors.
✅ Separate Printer, Scanner, Fax interfaces; an all-in-one implements all three, a basic printer implements one.
D — Dependency Inversion Principle
Depend on abstractions, not concrete classes.
❌ OrderService creates MySQLDatabase() inside itself → you can't test it without a real database or switch to PostgreSQL easily.
✅ Pass the dependency in (dependency injection):
class Store: # abstraction
def save(self, order): raise NotImplementedError
class MySQLStore(Store):
def save(self, order): return f"MySQL: saved {order}"
class FakeStore(Store): # for tests — no database needed
def save(self, order): return f"test: pretended to save {order}"
class OrderService:
def __init__(self, store: Store): # injected
self.store = store
def place(self, order):
return self.store.save(order)
print(OrderService(MySQLStore()).place("order#42"))
print(OrderService(FakeStore()).place("order#42"))
MySQL: saved order#42
test: pretended to save order#42
Frameworks like Spring (Java) do this injection for you.
Common mistakes
- Applying SOLID mechanically to tiny scripts — it's for code that keeps growing.
- Confusing Dependency Inversion (principle) with Dependency Injection (a technique to achieve it).
Quick recap
- S one reason to change · O extend, don't edit · L subclasses must be substitutable · I small interfaces · D depend on abstractions.
- Goal: change one thing without breaking others, and test easily.