AiTechWorlds
AiTechWorlds
Buildings are built to strict engineering codes. Not because engineers lack creativity, but because buildings that violate structural principles eventually collapse. The same is true for software.
Robert C. Martin ("Uncle Bob") formalized five principles for writing object-oriented software that is maintainable, extendable, and resistant to rot. Together, their initials spell SOLID. Each principle addresses a specific failure mode that causes codebases to become increasingly painful to work with over time.
These aren't abstract theory. Each principle fixes a concrete problem you've probably already encountered.
"A class should have one, and only one, reason to change."
Analogy: A chef cooks. An accountant manages finances. A janitor cleans. Imagine firing your chef because the restaurant's accounting was wrong. The roles are clearly separate — changing one shouldn't affect the other.
Violation:
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def save_to_database(self):
# BAD: User class knows about database internals
import sqlite3
conn = sqlite3.connect("users.db")
conn.execute("INSERT INTO users VALUES (?, ?)", (self.name, self.email))
conn.commit()
def send_welcome_email(self):
# BAD: User class knows about email system
print(f"Sending welcome email to {self.email}")
# SMTP logic here...
This User class has three reasons to change: if the user model changes, if the database changes, or if the email system changes. Any change to any of these three domains touches this one class.
Fixed:
class User:
# Responsibility: represent user data only
def __init__(self, name: str, email: str):
self.name = name
self.email = email
class UserRepository:
# Responsibility: persistence logic only
def save(self, user: User):
print(f"Saving {user.name} to database")
class WelcomeEmailService:
# Responsibility: email notification only
def send_welcome(self, user: User):
print(f"Sending welcome email to {user.email}")
Now each class has exactly one reason to change.
"Software entities should be open for extension, but closed for modification."
Analogy: A USB port is closed for modification (you can't change its shape) but open for extension (any USB device can plug in). New devices work without redesigning the port.
Violation:
class ReportGenerator:
def generate(self, data: list, format_type: str):
# BAD: Adding a new format requires editing this class
if format_type == "pdf":
print("Generating PDF...")
elif format_type == "csv":
print("Generating CSV...")
elif format_type == "excel":
print("Generating Excel...")
# Every new format = another elif = modifying this class
Fixed:
from abc import ABC, abstractmethod
class ReportFormatter(ABC):
@abstractmethod
def format(self, data: list) -> str:
pass
class PDFFormatter(ReportFormatter):
def format(self, data: list) -> str:
return f"PDF with {len(data)} rows"
class CSVFormatter(ReportFormatter):
def format(self, data: list) -> str:
return "\n".join([",".join(str(v) for v in row) for row in data])
class ReportGenerator:
# Closed for modification — never needs to change again
def generate(self, data: list, formatter: ReportFormatter) -> str:
return formatter.format(data) # Open for extension via new formatters
Adding Excel support means writing an ExcelFormatter class — zero changes to ReportGenerator.
"Subclasses must be substitutable for their base classes without breaking the program."
Analogy: If a recipe calls for "a bird," substituting a penguin (which cannot fly) would break a recipe that says "fly to the forest to find berries." Penguins are birds, but not substitutable in this context.
Violation:
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
# A Square IS-A Rectangle, right? Not quite...
def set_width(self, w):
self.width = w
self.height = w # Square forces width == height
def set_height(self, h):
self.width = h # This breaks Rectangle's contract!
self.height = h
def resize_rectangle(rect: Rectangle):
rect.set_width(5)
rect.set_height(10)
assert rect.area() == 50, "Expected area 50" # Fails for Square!
Fixed: If Square cannot be substituted for Rectangle, they should not share an inheritance relationship.
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
Both are Shape — both can compute area. Neither breaks the other's contract.
"Clients should not be forced to depend on interfaces they do not use."
Analogy: A waiter doesn't need to know how to repair the kitchen equipment. Forcing them to learn plumbing because it's in the "restaurant staff manual" is absurd.
Violation:
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass # Robots can't eat — but they'd be forced to implement this
@abstractmethod
def sleep(self): pass # Robots don't sleep either
Fixed — split into focused interfaces:
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class Sleepable(ABC):
@abstractmethod
def sleep(self): pass
class HumanWorker(Workable, Eatable, Sleepable):
def work(self): print("Human working")
def eat(self): print("Human eating lunch")
def sleep(self): print("Human sleeping")
class RobotWorker(Workable):
# Only implements what it actually needs
def work(self): print("Robot working 24/7")
Robots only implement Workable. They are never forced to provide a fake eat() implementation.
"Depend on abstractions, not concretions."
Analogy: Your laptop doesn't depend on a specific brand of charger. It depends on the USB-C standard (an abstraction). Any charger that meets the standard works.
Violation:
class MySQLDatabase:
def save(self, data): print(f"Saving {data} to MySQL")
class UserService:
def __init__(self):
# BAD: directly depends on MySQL — impossible to swap to PostgreSQL or mock
self.db = MySQLDatabase()
def create_user(self, name: str):
self.db.save(name)
Fixed:
from abc import ABC, abstractmethod
class Database(ABC):
# The abstraction — UserService depends on this, not on MySQL
@abstractmethod
def save(self, data: str): pass
class MySQLDatabase(Database):
def save(self, data: str): print(f"MySQL: saved '{data}'")
class PostgreSQLDatabase(Database):
def save(self, data: str): print(f"PostgreSQL: saved '{data}'")
class MockDatabase(Database):
# Used in tests — no real database needed
def __init__(self): self.saved = []
def save(self, data: str): self.saved.append(data)
class UserService:
def __init__(self, db: Database):
# Receives the dependency from outside — "injected"
self.db = db
def create_user(self, name: str):
self.db.save(name)
# In production:
service = UserService(MySQLDatabase())
service.create_user("Alice") # MySQL: saved 'Alice'
# In tests:
mock_db = MockDatabase()
test_service = UserService(mock_db)
test_service.create_user("TestUser")
print(mock_db.saved) # ['TestUser']
| Principle | Violation | Fix | Benefit |
|---|---|---|---|
| Single Responsibility | One class does everything | Split into focused classes | Easier to change, test, understand |
| Open/Closed | Add features by editing old code | Use abstraction + new classes | Never break working code |
| Liskov Substitution | Subclass breaks base class behavior | Ensure substitutability | Reliable polymorphism |
| Interface Segregation | Fat interfaces with unused methods | Split into small, focused interfaces | No empty/fake implementations |
| Dependency Inversion | Depend on concrete classes | Depend on abstractions, inject dependencies | Easy to swap, easy to test |
Buildings built to code don't collapse. Code built to SOLID doesn't collapse under its own weight.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises